├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── doc ├── api.md ├── config.md └── syntax.md ├── main.js ├── package.json ├── src ├── main.js ├── node.js └── tpl.js ├── test ├── etpl ├── lib │ ├── esl-1.6.6.js │ └── jasmine-1.3.0 │ │ ├── MIT.LICENSE │ │ ├── jasmine-html.js │ │ ├── jasmine.css │ │ └── jasmine.js ├── performance │ ├── artTemplate │ │ ├── artTemplate.html │ │ ├── artTemplate.js │ │ └── template.html │ ├── baiduTemplate │ │ ├── baiduTemplate.html │ │ ├── baiduTemplate.js │ │ └── template.html │ ├── compile-time-tpl.html │ ├── compile-time.html │ ├── dot │ │ ├── dot.html │ │ ├── dot.js │ │ └── template.html │ ├── ejs │ │ ├── ejs.html │ │ ├── ejs.js │ │ └── template.html │ ├── etpl │ │ ├── etpl.html │ │ └── template.html │ ├── handlebars │ │ ├── handlebars-1.1.2.js │ │ ├── handlebars.html │ │ └── template.html │ ├── hogan │ │ ├── hogan-2.0.0.js │ │ ├── hogan.html │ │ └── template.html │ ├── juicer │ │ ├── juicer.html │ │ ├── juicer.js │ │ └── template.html │ ├── load-template.js │ ├── loops.js │ ├── mustache │ │ ├── mustache.html │ │ ├── mustache.js │ │ └── template.html │ ├── nunjucks │ │ ├── nunjucks.html │ │ ├── nunjucks.js │ │ └── template.html │ ├── render-data.js │ └── render-time.html ├── run.html ├── server.js └── spec │ ├── amdplugin-absolute.text.html │ ├── amdplugin-relative.text.html │ ├── amdplugin.js │ ├── amdplugin.text.html │ ├── comment.spec.js │ ├── comment.text.html │ ├── engine.spec.js │ ├── engine.text.html │ ├── filter.spec.js │ ├── filter.text.html │ ├── for.spec.js │ ├── for.text.html │ ├── if.spec.js │ ├── if.text.html │ ├── import.spec.js │ ├── import.text.html │ ├── node.spec.js │ ├── node.spec.text │ ├── node │ ├── footer.etpl │ ├── header.etpl │ ├── main.etpl │ ├── master-main.etpl │ ├── master-root.etpl │ └── master-sub.etpl │ ├── readTextSync.js │ ├── target.spec.js │ ├── target.text.html │ ├── use.spec.js │ ├── use.text.html │ ├── var.spec.js │ ├── var.text.html │ ├── variableSubstitution.spec.js │ └── variableSubstitution.text.html └── tool └── dist.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [**.js] 10 | indent_style = space 11 | indent_size = 4 12 | 13 | [**.css] 14 | indent_style = space 15 | indent_size = 4 16 | 17 | [**.less] 18 | indent_style = space 19 | indent_size = 4 20 | 21 | [**.styl] 22 | indent_style = space 23 | indent_size = 4 24 | 25 | [**.html] 26 | indent_style = space 27 | indent_size = 4 28 | 29 | [**.tpl] 30 | indent_style = space 31 | indent_size = 4 32 | 33 | [**.json] 34 | indent_style = space 35 | indent_size = 4 36 | 37 | [*.md] 38 | trim_trailing_whitespace = false 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .idea/ 4 | npm-debug.log 5 | Thumbs.db 6 | .DS_Store 7 | *.swp 8 | *.gz 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /test 2 | /tool 3 | /dist 4 | npm-debug.log 5 | .DS_Store 6 | Thumbs.db -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | branches: 5 | only: 6 | - master -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Baidu Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use of this software in source and binary forms, with or 5 | without modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | Neither the name of Baidu Inc. nor the names of its contributors may be used 16 | to endorse or promote products derived from this software without specific 17 | prior written permission of Baidu Inc. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ETPL (Enterprise Template) 2 | 3 | [![Build Status](https://travis-ci.org/ecomfe/etpl.svg?branch=master)](https://travis-ci.org/ecomfe/etpl) 4 | 5 | ETPL是一个强复用、灵活、高性能的JavaScript模板引擎,适用于浏览器端或Node环境中视图的生成。 6 | 7 | [WebSite](http://ecomfe.github.io/etpl/) 8 | 9 | ## Download 10 | 11 | 除了通过github clone外,你可以通过`右键另存`的方式获得ETpl: 12 | 13 | - [压缩代码 (Compressed)](http://s1.bdstatic.com/r/www/cache/ecom/etpl/3-2-0/etpl.js) 14 | - [源码 (Source)](http://s1.bdstatic.com/r/www/cache/ecom/etpl/3-2-0/etpl.source.js) 15 | 16 | 也可以通过CDN引用: 17 | 18 | ```html 19 | 20 | ``` 21 | 22 | ## Start 23 | 24 | ETpl可以在`CommonJS/AMD`的模块定义环境中使用,也能直接在页面下通过`script`标签引用。 25 | 26 | 27 | ### 浏览器环境 28 | 29 | 直接通过script标签引用,你可以获得一个全局的`etpl`变量 30 | 31 | ```html 32 | 33 | ``` 34 | 35 | 在AMD环境的模块定义时,你可以通过`同步require`获得ETpl模块 36 | 37 | ```javascript 38 | define(function (require) { 39 | var etpl = require('etpl'); 40 | }); 41 | ``` 42 | 43 | 在AMD环境,你也可以通过`异步require`获得ETpl模块 44 | 45 | ```javascript 46 | require([ 'etpl' ], function (etpl) { 47 | }); 48 | ``` 49 | 50 | *在AMD环境下,请确保你的require.config配置能够让Loader找到ETpl模块* 51 | 52 | ### Node.JS环境 53 | 54 | 你可以通过`npm`来安装ETpl 55 | 56 | ``` 57 | $ npm install etpl 58 | ``` 59 | 60 | 安装完成后,你就可以通过`require`获得一个ETpl模块,正常地使用它 61 | 62 | ```javascript 63 | var etpl = require('etpl'); 64 | ``` 65 | 66 | ### 使用 67 | 68 | 使用ETPL模块,对模板源代码进行编译,会能得到编译后的function 69 | 70 | ```javascript 71 | var render = etpl.compile('Hello ${name}!'); 72 | ``` 73 | 74 | 执行这个function,传入数据对象,就能得到模板执行的结果了 75 | 76 | ```javascript 77 | var text = render({ name: 'etpl' }); 78 | ``` 79 | 80 | 查看更多例子,或者对模板渲染结果有疑虑,就去ETPL的[example](http://ecomfe.github.io/etpl/example.html)看看吧。 81 | 82 | 83 | ## Documents 84 | 85 | 通过文档,你可以更详细地了解ETpl的语法格式、使用方法、API等内容。 86 | 87 | - [模板语法](doc/syntax.md) 88 | - [API](doc/api.md) 89 | - [配置参数](doc/config.md) 90 | 91 | 92 | 93 | ## Related 94 | 95 | * Sublime Text 语法高亮插件:[sublime-etpl](https://github.com/ecomfe/sublime-etpl) 96 | * vim 语法高亮插件:[vim-etpl](https://github.com/hushicai/vim-etpl) 97 | * Atom 语法高亮插件:[atom-etpl](https://github.com/ecomfe/atom-etpl) 98 | 99 | 100 | 101 | ## Compatibility 102 | 103 | ### ETpl3的新语法 104 | 105 | 我们认为,当前流行的通过`block`来表达模板继承中的变化,是更好的表达方式。所以在ETpl3中,我们优化了母版的语法,删除了`master`、`contentplacehoder`、`content`标签,引入了`block`标签。 106 | 107 | 对于ETpl2的使用者,我们提供一个[etpl2to3](https://github.com/ecomfe/etpl2to3)工具,能够帮助你平滑地将ETpl2的模板翻译成ETpl3。 108 | 109 | 110 | ### get 111 | 112 | ETpl2中,为了前向兼容,Engine的`get`方法可以根据target名称获取模板内容。 113 | 114 | ETpl3不再支持该方法,所有的模板都通过render来使用: 115 | 116 | - 直接使用engine实例的render方法 117 | - 调用renderer function 118 | 119 | 如果仍需要该功能,说明你正在维护一个遗留系统,并且没有很频繁的升级需求。请继续使用ETpl2。 120 | 121 | 122 | ### merge 123 | 124 | ETpl的前身是[ER框架](https://github.com/ecomfe/er)自带的简易模板引擎,其基本与前身保持兼容。但出于代码体积和使用频度的考虑,ETpl删除了`merge`API。如果想要该API,请在自己的应用中加入如下代码: 125 | 126 | ```javascript 127 | /** 128 | * 执行模板渲染,并将渲染后的字符串作为innerHTML填充到HTML元素中。 129 | * 兼容老版本的模板引擎api 130 | * 131 | * @param {HTMLElement} element 渲染字符串填充的HTML元素 132 | * @param {string} name target名称 133 | * @param {Object=} data 模板数据 134 | */ 135 | etpl.merge = function ( element, name, data ) { 136 | if ( element ) { 137 | element.innerHTML = this.render( name, data ); 138 | } 139 | }; 140 | ``` 141 | 142 | -------------------------------------------------------------------------------- /doc/api.md: -------------------------------------------------------------------------------- 1 | ## API 2 | 3 | ETpl 是一个多 Engine 实例的模板引擎。初始化时其自动创建一个默认的 Engine 实例,并将其暴露。大多数应用场景可直接使用默认的引擎实例。 4 | 5 | ```javascript 6 | var etpl = require( 'etpl' ); 7 | ``` 8 | 9 | 你也可以通过 `new etpl.Engine` 初始化自己的引擎实例。 *不同的引擎实例可有效避免target命名冲突的问题。* 10 | 11 | ```javascript 12 | var etpl = require( 'etpl' ); 13 | var etplEngine = new etpl.Engine(); 14 | ``` 15 | 16 | 引擎实例的初始化允许传入引擎参数。支持的引擎参数见下面的`config`方法。 17 | 18 | ```javascript 19 | var etpl = require( 'etpl' ); 20 | var etplEngine = new etpl.Engine({ 21 | commandOpen: '<%', 22 | commandClose: '%>' 23 | }); 24 | ``` 25 | 26 | 下面是 Engine 的实例方法列表。 27 | 28 | - [addCommand](#addCommand) 29 | - [addFilter](#addFilter) 30 | - [compile](#compile) 31 | - [config](#config) 32 | - [getRenderer](#getRenderer) 33 | - [load](#load) 34 | - [loadFromFile](#loadFromFile) 35 | - [parse](#parse) 36 | - [render](#render) 37 | 38 | 39 | 40 | ### addCommand 41 | 42 | `{void} addCommand( {string}name, {Object}command )` 43 | 44 | `常用` 自定义命令标签。 45 | 46 | - `{string}`name - 命令标签名称 47 | - `{Object}`command - 命令对象 48 | 49 | command 参数是一个对象,包含一些在命令标签编译时各个阶段的处理方法。 50 | 51 | - `init(context)` - 初始化 52 | - `open(context)` - 标签起始 53 | - `close(context)` - 标签闭合 54 | - `getRendererBody():string` - 生成编译代码 55 | 56 | ```javascript 57 | etpl.addCommand('dump', { 58 | init: function () { 59 | var match = this.value.match(/^\s*([a-z0-9_]+)\s*$/i); 60 | if (!match) { 61 | throw new Error('Invalid ' + this.type + ' syntax: ' + this.value); 62 | } 63 | 64 | this.name = match[1]; 65 | this.cloneProps = ['name']; 66 | }, 67 | 68 | open: function (context) { 69 | context.stack.top().addChild(this); 70 | }, 71 | 72 | getRendererBody: function () { 73 | var util = etpl.util; 74 | var options = this.engine.options; 75 | return util.stringFormat( 76 | 'console.log({0});', 77 | util.compileVariable(options.variableOpen + this.name + options.variableClose, this.engine) 78 | ); 79 | } 80 | }); 81 | ``` 82 | 83 | 84 | ### addFilter 85 | 86 | `{void} addFilter( {string}name, {function({string}, {...*}):string}filter )` 87 | 88 | `常用` 为默认引擎添加过滤器。过滤函数的第一个参数为过滤源字符串,其后的参数可由模板开发者传入。过滤函数必须返回string。 89 | 90 | - `{string}`name - 过滤器名称 91 | - `{Function}`filter - 过滤函数 92 | 93 | ```javascript 94 | etpl.addFilter( 'markdown', function ( source, useExtra ) { 95 | // ...... 96 | } ); 97 | ``` 98 | 99 | ### compile 100 | 101 | `{Function} compile( {string}source )` 102 | 103 | `常用` 使用默认引擎编译模板。返回第一个target编译后的renderer函数。 104 | 105 | - `{string}`source - 模板源代码 106 | 107 | ```javascript 108 | var helloRenderer = etpl.compile( 'Hello ${name}!' ); 109 | helloRenderer( {name: 'ETPL'} ); // Hello ETPL! 110 | ``` 111 | 112 | ### config 113 | 114 | `{void} config( {Object}options )` 115 | 116 | `常用` 对默认引擎进行配置,配置参数将合并到引擎现有的参数中。[查看配置参数](config.md)。 117 | 118 | ```javascript 119 | etplEngine.config( { 120 | defaultFilter: '' 121 | } ); 122 | ``` 123 | 124 | ### getRenderer 125 | 126 | `{Function} getRenderer( {string}name )` 127 | 128 | `常用` 从默认引擎中,根据target名称获取编译后的renderer函数。 129 | 130 | - `{string}`name - target名称 131 | 132 | ```javascript 133 | etpl.compile( 'Hello ${name}!' ); 134 | var helloRenderer = etpl.getRenderer( 'hello' ); 135 | helloRenderer( {name: 'ETPL'} ); // Hello ETPL! 136 | ``` 137 | 138 | 139 | ### load 140 | 141 | `{Function} load( {string}name )` 142 | 143 | `仅node环境` 加载并编译target文件 144 | 145 | - `{string}`name - target名称 146 | 147 | ```javascript 148 | var mainRenderer = etpl.load( 'main' ); 149 | mainRenderer( {name: 'ETPL'} ); 150 | ``` 151 | 152 | 153 | ### loadFromFile 154 | 155 | `{Function} loadFromFile( {string}file )` 156 | 157 | `仅node环境` 加载并编译模板文件 158 | 159 | - `{string}`file - 模板文件路径 160 | 161 | ```javascript 162 | var mainRenderer = etpl.loadFromFile( path.resolve(__dirname, 'main.etpl') ); 163 | mainRenderer( {name: 'ETPL'} ); 164 | ``` 165 | 166 | 167 | ### parse 168 | 169 | 同`compile`方法。该方法的存在是为了兼容老版本的模板引擎api,不建议使用。 170 | 171 | 172 | ### render 173 | 174 | `{string} render( {string}name, {Object}data )` 175 | 176 | 使用默认引擎执行模板渲染,返回渲染后的字符串。 177 | 178 | - `{string}`name - target名称 179 | - `{Object}`data - 模板数据。可以是plain object,也可以是带有 **{string}get({string}name)** 方法的对象 180 | 181 | ```javascript 182 | etpl.compile( 'Hello ${name}!' ); 183 | etpl.render( 'hello', {name: 'ETPL'} ); // Hello ETPL! 184 | ``` 185 | 186 | 187 | -------------------------------------------------------------------------------- /doc/config.md: -------------------------------------------------------------------------------- 1 | ## Config 2 | 3 | - [commandOpen](#commandopen) 4 | - [commandClose](#commandclose) 5 | - [commandSyntax](#commandsyntax) 6 | - [variableOpen](#variableopen) 7 | - [variableClose](#variableclose) 8 | - [defaultFilter](#defaultfilter) 9 | - [strip](#strip) 10 | - [namingConflict](#namingconflict) 11 | - [missTarget](#misstarget) 12 | - [dir](#dir) 13 | - [extname](#extname) 14 | - [encoding](#encoding) 15 | 16 | 17 | 通过engine初始化时构造函数参数,或者engine实例的config方法,可以配置ETpl引擎的参数。 18 | 19 | ```javascript 20 | etplEngine.config( { 21 | strip: true 22 | } ); 23 | ``` 24 | 25 | 下面是ETpl支持的参数列表。 26 | 27 | 28 | ### commandOpen 29 | 30 | `string` 31 | 32 | 命令语法起始串,默认值为 ** 40 | 41 | 42 | ### commandSyntax 43 | 44 | `RegExp` 45 | 46 | 命令语法格式。设置该参数时,正则需要包含3个matches: 47 | 48 | 1. 命令结束标记 49 | 2. 命令名称 50 | 3. 命令值 51 | 52 | 53 | ### variableOpen 54 | 55 | `string`,默认值为 *${* 56 | 57 | 变量语法起始串 58 | 59 | 60 | ### variableClose 61 | 62 | `string` 63 | 64 | 变量语法结束串,默认值为 *}* 65 | 66 | 67 | ### defaultFilter 68 | 69 | `string` 70 | 71 | 默认变量替换的filter,默认值为 *html* 72 | 73 | 74 | ### strip 75 | 76 | `boolean` 77 | 78 | 是否清除命令标签前后的空白字符,默认值为 *false* 79 | 80 | 81 | ### namingConflict 82 | 83 | `string` 84 | 85 | target名字冲突时的处理策略,值可以是: 86 | 87 | - `error`: 抛出错误。此项为默认值 88 | - `ignore`: 保留现有目标,忽略新目标 89 | - `override`: 覆盖现有目标 90 | 91 | ### missTarget 92 | 93 | `string` 94 | 95 | target不存在时的处理策略,值可以是: 96 | 97 | - `error`: 抛出错误 98 | - `ignore`: 静默处理,无错误。此项为默认值 99 | 100 | 101 | ### dir 102 | 103 | `string` `仅node环境有效` 104 | 105 | 模板文件目录,用于加载target文件时的路径查找,默认值为 *process.pwd()* 106 | 107 | 108 | ### extname 109 | 110 | `string` `仅node环境有效` 111 | 112 | 模板文件后缀名,默认值为 *.etpl* 113 | 114 | 115 | ### encoding 116 | 117 | `string` `仅node环境有效` 118 | 119 | 模板文件编码,默认值为 *UTF-8* 120 | 121 | 122 | -------------------------------------------------------------------------------- /doc/syntax.md: -------------------------------------------------------------------------------- 1 | ## Syntax 2 | 3 | 4 | - [基础](#基础) 5 | - [语法形式](#语法形式) 6 | - [target](#target) 7 | - [变量替换](#变量替换) 8 | - [模板复用](#模板复用) 9 | - [import](#import) 10 | - [母版](#母版) 11 | - [引用代入](#引用代入) 12 | - [use](#use) 13 | - [分支与循环](#分支与循环) 14 | - [if](#if) 15 | - [for](#for) 16 | - [其他](其他) 17 | - [变量声明](#变量声明) 18 | - [内容块过滤](#内容块过滤) 19 | 20 | 21 | ### 基础 22 | 23 | #### 语法形式 24 | 25 | ETPL的指令标签默认为HTML注释的形式,在指令标签内允许声明 `指令起始`、`指令结束`和`注释`。 26 | 27 | `指令起始`的语法形式为: **。其中, *command-value* 的具体语法形式详情请参见各指令相关章节。 28 | 29 | ```html 30 | 31 | 32 | 33 | ``` 34 | 35 | `指令结束`的语法形式为: **。 36 | 37 | ```html 38 | 39 | 40 | 41 | ``` 42 | 43 | `注释`的语法形式为: **,注释指令在render时将不输出。 44 | 45 | ```html 46 | 47 | ``` 48 | 49 | 如果不期望使用HTML注释形式的指令标签,可以通过config API可以配置指令标签的形式: 50 | 51 | ```javascript 52 | etpl.config({ 53 | commandOpen: '<%', 54 | commandClose: '%>' 55 | }); 56 | 57 | /* 58 | 配置指令标签的形式为“<% ... %>”,然后指令标签可以像下面一样声明: 59 | <% if: ${number} > 0 %> 60 | greater than zero 61 | <% /if %> 62 | */ 63 | ``` 64 | 65 | ##### 自动结束 66 | 67 | 为了减少开发者的工作量,部分指令标签支持`自动结束`,模板开发者无需手工编写`指令结束`。比如:当遇见target指令起始时,ETPL自动认为上一个target已经结束。 68 | 69 | 具体指令的`自动结束`支持,请参考相应指令相关章节。 70 | 71 | 72 | #### target 73 | 74 | `target`是ETPL的基本单元,其含义是 **一个模版片段** 。`target`可用于render,也可用于被其他`target`所import或use,也可以被其他`target`以母版的方式继承。 75 | 76 | 在target中可以任意编写`block`片段。`block`主要的作用是做为 **可被替换片段** ,在母版或引用代入功能中被使用。详细的使用方式请参考[模板复用](#模板复用)章节。 77 | 78 | 79 | ##### 语法 80 | 81 | target的语法形式为: 82 | 83 | target: target-name 84 | target: target-name(master=master-name) 85 | 86 | 87 | block的语法形式为: 88 | 89 | block: block-name 90 | 91 | 92 | ##### 自动结束 93 | 94 | target支持自动结束,当遇见 *target* 时自动结束。 95 | 96 | block不支持自动结束,必须手工编写`指令结束`。 97 | 98 | ##### 示例 99 | 100 | ```html 101 | 102 | Hello ETPL! 103 | 104 | 105 | Bye ETPL! 106 | 107 | 108 |
109 | Header Content 110 |
111 |
112 | ``` 113 | 114 | ##### 匿名target 115 | 116 | 如果仅仅编写的是一个模板片段,可以省略`target`的声明。这样的编写方式与其他模板引擎类似,ETPL将默认生成匿名target,但模板片段将不可复用(不可被import或use,不可指定母版)。 117 | 118 | 匿名target应位于模板源码起始。下面例子中,位于其他target后的模板片段`Bye`将没有任何作用。 119 | 120 | ``` 121 | 122 | 123 | Hello ${name}! 124 | 125 | Bye 126 | ``` 127 | 128 | #### 变量替换 129 | 130 | 绝大多数模板引擎都支持变量替换功能。ETPL变量替换的语法为: 131 | 132 | ${variable-name} 133 | ${variable-name|filter-name} 134 | ${variable-name|filter-name(arguments)} 135 | ${variable-name|filter1|filter2(arguments)|filter3|...} 136 | 137 | variable-name支持`.`形式的property access。 138 | 139 | 编写模板时可以指定filter,默认使用html filter进行HTML转义。如果想要保留变量的原形式,需要手工指定使用名称为raw的filter,或者通过config API配置引擎的defaultFilter参数。 140 | 141 | ${myVariable|raw} 142 | 143 | ```javascript 144 | etpl.config({ 145 | defaultFilter: '' 146 | }); 147 | ``` 148 | 149 | ETPL默认支持3种filters,可以通过引擎的`addFilter`方法自定义扩展filter。 150 | 151 | - html: html转义 152 | - url: url转义 153 | - raw: 不进行任何转义 154 | 155 | 156 | 变量替换支持多filter处理。filter之间以类似管道的方式,前一个filter的输出做为后一个filter的输入,向后传递。 157 | 158 | ```html 159 | ${myVariable|html|url} 160 | ``` 161 | 162 | filter支持参数传递,参数可以使用动态数据。 163 | 164 | ```html 165 | 166 | ${myVariable|comma(3)} 167 | ${myVariable|comma(${commaLength})} 168 | ${myVariable|comma(${commaLength}+1)} 169 | ``` 170 | 171 | 在变量替换中,引擎会默认将数据toString后传递给filter,以保证filter输入输出的类型一致性。如果filter期望接受的是原始数据,模板开发者需要通过前缀的`*`指定。 172 | 173 | ```html 174 | 175 | ${*myDate|dateFormat('yyyy-MM-dd')} 176 | ``` 177 | 178 | 179 | 180 | ### 模板复用 181 | 182 | ETPL支持多种形式的模板复用方式,帮助模板开发者减少模板编写的重复劳动和维护成本。 183 | 184 | 185 | #### import 186 | 187 | 通过import指令,可以在当前位置插入指定target的源码。 188 | 189 | ##### 语法 190 | 191 | import的语法形式为: 192 | 193 | import: target-name 194 | 195 | 196 | ##### 示例 197 | 198 | ```html 199 | 200 | Hello ${name}! 201 | 202 | 203 |
204 | ``` 205 | 206 | ##### 自动结束 207 | 208 | 如果不编写import的`指令结束`,其`指令起始`后立即自动结束。 209 | 210 | 如果需要使用import的[引用代入](#引用代入)功能,需要手工编写`指令结束`。 211 | 212 | 213 | #### 母版 214 | 215 | 216 | `target`声明时可以通过 **master=master-name** 指定一个继承的母版。并且通过自身的`block`指令,能够替换母版中同名`block`指令声明部分的内容。 217 | 218 | 模版继承(母版)功能允许你在一开始就对多个相似的模板从“结构”的角度进行抽象。继承自一个和你具有相同结构的母版,你可以无需再编写结构骨架部分,只需要编写结构内部的内容差异部分。 219 | 220 | 221 | ##### 示例 222 | 223 | ```html 224 | 225 | 226 | Hello ${name}! 227 | 228 | 229 | 230 |
Header Content
231 |
232 | ``` 233 | 234 | ##### 复杂示例 235 | 236 | ```html 237 | 238 |
239 | title 240 |
241 |
242 | 243 |
244 | 245 | 246 | title for has sidebar 247 | 248 | 249 |
250 | 251 | 252 | 253 | 254 | Building WebKit from Xcode 255 | 256 | 257 |

To build from within Xcode, you can use the WebKit workspace.

258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 |

To build from within Xcode, you can use the WebKit workspace.

266 | 267 | ``` 268 | 269 | 270 | #### 引用代入 271 | 272 | 通过`import`指令引入一个`target`时,可以定制其`block`部分的内容。 273 | 274 | 如果有一个模板的大部分可以复用时,通过引用代入功能,你可以在引用时抹平差异,定制其中差异部分内容。当你面临结构种类数量爆炸时,引用代入提供了另一种更灵活的复用方式。 275 | 276 | 使用引用代入功能,import需要手工编写`指令结束`,并且import中只允许包含`block`,其他指令将被忽略。 277 | 278 | ##### 示例 279 | 280 | ```html 281 | 282 | 283 | 284 | 285 |
list
286 |
pager
287 | 288 | 289 | 290 | 291 |
default header
292 | 293 | 294 |
295 | default main 296 |
297 | ``` 298 | 299 | 300 | 301 | #### use 302 | 303 | 通过`use`指令,可以调用指定`target`,在当前位置插入其render后的结果。允许使用静态或动态数据指定数据项。 304 | 305 | 在面临多种不同的数据名称、相似的结构时,动态调用可以让你只编写一份代码,多次调用。 306 | 307 | ##### 语法 308 | 309 | use的语法形式为: 310 | 311 | use: target-name 312 | use: target-name(data-name=expression, data-name=expression) 313 | 314 | 315 | ##### 示例 316 | 317 | ```html 318 | 323 | 324 |
  • ${main}[${sub}]
  • 325 | ``` 326 | 327 | ##### 自动结束 328 | 329 | use无需编写`指令结束`,其将在`指令起始`后立即自动结束。 330 | 331 | 332 | 333 | ### 分支与循环 334 | 335 | #### if 336 | 337 | ETPL提供了分支的支持,相关指令有`if`、`elif`、`else`。 338 | 339 | ##### 语法 340 | 341 | if的语法形式为: 342 | 343 | if: conditional-expression 344 | 345 | elif的语法形式为: 346 | 347 | elif: conditional-expression 348 | 349 | else的语法形式为: 350 | 351 | else 352 | 353 | conditional-expression中可以使用动态数据,通过`${variable}`的形式,可以使用模板render的data。`${variable}`支持`.`的property accessor。 354 | 355 | ##### 自动结束 356 | 357 | if指令不支持自动结束,必须手工编写指令结束``。 358 | 359 | ##### 示例 360 | 361 | ```html 362 | 363 | larger than zero 364 | 365 | zero 366 | 367 | invalid 368 | 369 | ``` 370 | 371 | ##### 自动结束 372 | 373 | if指令不支持自动结束,必须手工编写`指令结束`。 374 | 375 | ```html 376 | 377 | ``` 378 | 379 | #### for 380 | 381 | 通过for指令的支持,可以实现对Array和Object的遍历。Array为正序遍历,Object为不保证顺序的forin。 382 | 383 | ##### 语法 384 | 385 | for的语法形式为: 386 | 387 | for: ${variable} as ${item-variable} 388 | for: ${variable} as ${item-variable}, ${index-variable} 389 | for: ${variable} as ${item-variable}, ${key-variable} 390 | 391 | 其中,`${variable}`为想要遍历的对象,支持`.`形式的property access。在遍历过程中,声明的`${item-variable}`和`${index-variable}`,分别代表数据项和索引(遍历Object时为键名)。 392 | 393 | ##### 示例 394 | 395 | ```html 396 | 401 | ``` 402 | 403 | ##### 自动结束 404 | 405 | for指令不支持自动结束,必须手工编写`指令结束`。 406 | 407 | ```html 408 | 409 | ``` 410 | 411 | ### 其他 412 | 413 | #### 变量声明 414 | 415 | 通过`var`指令可以在模板内部声明一个变量。声明的变量在整个`target`内有效。 416 | 417 | ##### 语法 418 | 419 | var的语法形式为: 420 | 421 | var: var-name=expression 422 | 423 | `expression`中可以使用静态或动态数据。 424 | 425 | ##### 示例 426 | 427 | ```html 428 | 429 | 430 | 431 | ``` 432 | 433 | ##### 自动结束 434 | 435 | var无需编写`指令结束`,其将在`指令起始`后立即自动结束。 436 | 437 | 438 | #### 内容块过滤 439 | 440 | 除了在变量替换中可以使用filter进行处理,ETPL还可以通过`filter`指令,使用指定的filter对一个模板内容块进行过滤处理。 441 | 442 | ##### 语法 443 | 444 | filter的语法形式为: 445 | 446 | filter: filter-name 447 | filter: filter-name(arguments) 448 | 449 | ##### 示例 450 | 451 | 下面的例子假定使用者实现了一个markdown的filter 452 | 453 | ```html 454 | 455 | ## markdown document 456 | 457 | This is the content, also I can use `${variables}` 458 | 459 | ``` 460 | 461 | ##### 自动结束 462 | 463 | filter指令不支持自动结束,必须手工编写`指令结束`。 464 | 465 | ```html 466 | 467 | ``` 468 | 469 | ##### 表达式的特殊语法( expression 和 condition-expression ) 470 | 471 | `if`指令、`use`指令和`var`指令中的表达式( expression 或者 condition-expression )部分,可以使用动态的数据: 472 | 473 | ```html 474 | He's name is zhangsan. 475 | 476 | ${personName} 477 | 478 | 479 | ${name} 480 | ``` 481 | 482 | 如果传入的数据是: 483 | 484 | ```json 485 | { 486 | "arr": ["zhangsan"], 487 | "index": 0 488 | } 489 | ``` 490 | 491 | 输出的结果为: 492 | 493 | ``` 494 | He's name is zhangsan. 495 | 496 | zhangsan 497 | 498 | zhangsan 499 | ``` 500 | 501 | 其它语法示例: 502 | 503 | ```html 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | ``` 516 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ETPL (Enterprise Template) 3 | * Copyright 2013 Baidu Inc. All rights reserved. 4 | * 5 | * @file Node入口 6 | * @author firede(firede@firede.us) 7 | */ 8 | 9 | module.exports = require('./src/node'); 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "etpl", 3 | "version": "3.2.0", 4 | "contributors": [ 5 | { "name": "erik", "email": "errorrik@gmail.com" }, 6 | { "name": "otakustay", "email": "otakustay@gmail.com" }, 7 | { "name": "firede", "email": "firede@firede.us" } 8 | ], 9 | "main": "main", 10 | "homepage": "http://ecomfe.github.io/etpl/", 11 | "repository": "git://github.com/ecomfe/etpl", 12 | "description": "ETPL是一个强复用、灵活、高性能的JavaScript模板引擎,适用于浏览器端或Node环境中视图的生成。", 13 | "scripts": { 14 | "test": "jasmine-node test/spec" 15 | }, 16 | "devDependencies": { 17 | "jasmine-node": "1.14.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ETPL (Enterprise Template) 3 | * Copyright 2016 Baidu Inc. All rights reserved. 4 | * 5 | * @file node环境支持模块,主要提供import和master的依赖文件自动加载功能 6 | * @author errorrik(errorrik@gmail.com) 7 | */ 8 | 9 | 10 | /* eslint-env node */ 11 | 12 | var etpl = require('./main'); 13 | var path = require('path'); 14 | var fs = require('fs'); 15 | 16 | /** 17 | * 加载模板文件 18 | * 19 | * @param {string} file 文件路径 20 | * @return {function(Object):string} renderer函数 21 | */ 22 | etpl.Engine.prototype.loadFromFile = function (file) { 23 | var load = this.load.bind(this); 24 | var targets = this.targets; 25 | 26 | /* jshint -W054 */ 27 | var renderer = new Function('return ""'); 28 | /* jshint +W054 */ 29 | 30 | var encoding = this.options.encoding || 'UTF-8'; 31 | var source = fs.readFileSync(file, encoding); 32 | 33 | var parseInfo = etpl.util.parseSource(source, this); 34 | var targetNames = parseInfo.targets; 35 | 36 | 37 | parseInfo.deps.forEach(function (dep) { 38 | if (!targets[dep]) { 39 | load(dep); 40 | } 41 | }); 42 | 43 | if (targetNames.length) { 44 | renderer = targets[targetNames[0]].getRenderer(); 45 | } 46 | 47 | return renderer; 48 | }; 49 | 50 | /** 51 | * 加载target 52 | * 53 | * @param {string} targetName target名称 54 | * @return {function(Object):string} renderer函数 55 | */ 56 | etpl.Engine.prototype.load = function (targetName) { 57 | if (this.targets[targetName]) { 58 | return this.targets[targetName].getRenderer(); 59 | } 60 | 61 | return this.loadFromFile(resolveTargetPath(targetName, this)); 62 | }; 63 | 64 | 65 | /** 66 | * 获取 target 对应的文件路径 67 | * 68 | * @inner 69 | * @param {string} targetName target名称 70 | * @param {Engine} engine etpl引擎 71 | * @return {string} 72 | */ 73 | function resolveTargetPath(targetName, engine) { 74 | var dir = engine.options.dir || process.pwd(); 75 | var ext = engine.options.extname || '.etpl'; 76 | 77 | return path.resolve(dir, targetName) + ext; 78 | } 79 | 80 | 81 | module.exports = exports = etpl; 82 | -------------------------------------------------------------------------------- /src/tpl.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ETPL (Enterprise Template) 3 | * Copyright 2013 Baidu Inc. All rights reserved. 4 | * 5 | * @file 加载模板的amd模块 6 | * @author errorrik(errorrik@gmail.com) 7 | */ 8 | 9 | /* global ActiveXObject */ 10 | define( 11 | function (require, exports, module) { 12 | var etpl = require('.'); 13 | 14 | return { 15 | load: function (resourceId, req, load) { 16 | var xhr = window.XMLHttpRequest 17 | ? new XMLHttpRequest() 18 | : new ActiveXObject('Microsoft.XMLHTTP'); 19 | 20 | xhr.open('GET', req.toUrl(resourceId), true); 21 | 22 | xhr.onreadystatechange = function () { 23 | if (xhr.readyState === 4) { 24 | if (xhr.status >= 200 && xhr.status < 300) { 25 | var source = xhr.responseText; 26 | var moduleConfig = module.config(); 27 | if (moduleConfig.autoCompile 28 | || moduleConfig.autoCompile == null 29 | ) { 30 | etpl.compile(source); 31 | } 32 | 33 | load(source); 34 | } 35 | 36 | /* jshint -W054 */ 37 | xhr.onreadystatechange = new Function(); 38 | /* jshint +W054 */ 39 | xhr = null; 40 | } 41 | }; 42 | 43 | xhr.send(null); 44 | } 45 | }; 46 | } 47 | ); 48 | -------------------------------------------------------------------------------- /test/etpl: -------------------------------------------------------------------------------- 1 | ../src -------------------------------------------------------------------------------- /test/lib/jasmine-1.3.0/MIT.LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2011 Pivotal Labs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /test/lib/jasmine-1.3.0/jasmine.css: -------------------------------------------------------------------------------- 1 | body { background-color: #eeeeee; padding: 0; margin: 5px; overflow-y: scroll; } 2 | 3 | #HTMLReporter { font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; } 4 | #HTMLReporter a { text-decoration: none; } 5 | #HTMLReporter a:hover { text-decoration: underline; } 6 | #HTMLReporter p, #HTMLReporter h1, #HTMLReporter h2, #HTMLReporter h3, #HTMLReporter h4, #HTMLReporter h5, #HTMLReporter h6 { margin: 0; line-height: 14px; } 7 | #HTMLReporter .banner, #HTMLReporter .symbolSummary, #HTMLReporter .summary, #HTMLReporter .resultMessage, #HTMLReporter .specDetail .description, #HTMLReporter .alert .bar, #HTMLReporter .stackTrace { padding-left: 9px; padding-right: 9px; } 8 | #HTMLReporter #jasmine_content { position: fixed; right: 100%; } 9 | #HTMLReporter .version { color: #aaaaaa; } 10 | #HTMLReporter .banner { margin-top: 14px; } 11 | #HTMLReporter .duration { color: #aaaaaa; float: right; } 12 | #HTMLReporter .symbolSummary { overflow: hidden; *zoom: 1; margin: 14px 0; } 13 | #HTMLReporter .symbolSummary li { display: block; float: left; height: 7px; width: 14px; margin-bottom: 7px; font-size: 16px; } 14 | #HTMLReporter .symbolSummary li.passed { font-size: 14px; } 15 | #HTMLReporter .symbolSummary li.passed:before { color: #5e7d00; content: "\02022"; } 16 | #HTMLReporter .symbolSummary li.failed { line-height: 9px; } 17 | #HTMLReporter .symbolSummary li.failed:before { color: #b03911; content: "x"; font-weight: bold; margin-left: -1px; } 18 | #HTMLReporter .symbolSummary li.skipped { font-size: 14px; } 19 | #HTMLReporter .symbolSummary li.skipped:before { color: #bababa; content: "\02022"; } 20 | #HTMLReporter .symbolSummary li.pending { line-height: 11px; } 21 | #HTMLReporter .symbolSummary li.pending:before { color: #aaaaaa; content: "-"; } 22 | #HTMLReporter .exceptions { color: #fff; float: right; margin-top: 5px; margin-right: 5px; } 23 | #HTMLReporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; } 24 | #HTMLReporter .runningAlert { background-color: #666666; } 25 | #HTMLReporter .skippedAlert { background-color: #aaaaaa; } 26 | #HTMLReporter .skippedAlert:first-child { background-color: #333333; } 27 | #HTMLReporter .skippedAlert:hover { text-decoration: none; color: white; text-decoration: underline; } 28 | #HTMLReporter .passingAlert { background-color: #a6b779; } 29 | #HTMLReporter .passingAlert:first-child { background-color: #5e7d00; } 30 | #HTMLReporter .failingAlert { background-color: #cf867e; } 31 | #HTMLReporter .failingAlert:first-child { background-color: #b03911; } 32 | #HTMLReporter .results { margin-top: 14px; } 33 | #HTMLReporter #details { display: none; } 34 | #HTMLReporter .resultsMenu, #HTMLReporter .resultsMenu a { background-color: #fff; color: #333333; } 35 | #HTMLReporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; } 36 | #HTMLReporter.showDetails .summaryMenuItem:hover { text-decoration: underline; } 37 | #HTMLReporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; } 38 | #HTMLReporter.showDetails .summary { display: none; } 39 | #HTMLReporter.showDetails #details { display: block; } 40 | #HTMLReporter .summaryMenuItem { font-weight: bold; text-decoration: underline; } 41 | #HTMLReporter .summary { margin-top: 14px; } 42 | #HTMLReporter .summary .suite .suite, #HTMLReporter .summary .specSummary { margin-left: 14px; } 43 | #HTMLReporter .summary .specSummary.passed a { color: #5e7d00; } 44 | #HTMLReporter .summary .specSummary.failed a { color: #b03911; } 45 | #HTMLReporter .description + .suite { margin-top: 0; } 46 | #HTMLReporter .suite { margin-top: 14px; } 47 | #HTMLReporter .suite a { color: #333333; } 48 | #HTMLReporter #details .specDetail { margin-bottom: 28px; } 49 | #HTMLReporter #details .specDetail .description { display: block; color: white; background-color: #b03911; } 50 | #HTMLReporter .resultMessage { padding-top: 14px; color: #333333; } 51 | #HTMLReporter .resultMessage span.result { display: block; } 52 | #HTMLReporter .stackTrace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; } 53 | 54 | #TrivialReporter { padding: 8px 13px; position: absolute; top: 0; bottom: 0; left: 0; right: 0; overflow-y: scroll; background-color: white; font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; /*.resultMessage {*/ /*white-space: pre;*/ /*}*/ } 55 | #TrivialReporter a:visited, #TrivialReporter a { color: #303; } 56 | #TrivialReporter a:hover, #TrivialReporter a:active { color: blue; } 57 | #TrivialReporter .run_spec { float: right; padding-right: 5px; font-size: .8em; text-decoration: none; } 58 | #TrivialReporter .banner { color: #303; background-color: #fef; padding: 5px; } 59 | #TrivialReporter .logo { float: left; font-size: 1.1em; padding-left: 5px; } 60 | #TrivialReporter .logo .version { font-size: .6em; padding-left: 1em; } 61 | #TrivialReporter .runner.running { background-color: yellow; } 62 | #TrivialReporter .options { text-align: right; font-size: .8em; } 63 | #TrivialReporter .suite { border: 1px outset gray; margin: 5px 0; padding-left: 1em; } 64 | #TrivialReporter .suite .suite { margin: 5px; } 65 | #TrivialReporter .suite.passed { background-color: #dfd; } 66 | #TrivialReporter .suite.failed { background-color: #fdd; } 67 | #TrivialReporter .spec { margin: 5px; padding-left: 1em; clear: both; } 68 | #TrivialReporter .spec.failed, #TrivialReporter .spec.passed, #TrivialReporter .spec.skipped { padding-bottom: 5px; border: 1px solid gray; } 69 | #TrivialReporter .spec.failed { background-color: #fbb; border-color: red; } 70 | #TrivialReporter .spec.passed { background-color: #bfb; border-color: green; } 71 | #TrivialReporter .spec.skipped { background-color: #bbb; } 72 | #TrivialReporter .messages { border-left: 1px dashed gray; padding-left: 1em; padding-right: 1em; } 73 | #TrivialReporter .passed { background-color: #cfc; display: none; } 74 | #TrivialReporter .failed { background-color: #fbb; } 75 | #TrivialReporter .skipped { color: #777; background-color: #eee; display: none; } 76 | #TrivialReporter .resultMessage span.result { display: block; line-height: 2em; color: black; } 77 | #TrivialReporter .resultMessage .mismatch { color: black; } 78 | #TrivialReporter .stackTrace { white-space: pre; font-size: .8em; margin-left: 10px; max-height: 5em; overflow: auto; border: 1px inset red; padding: 1em; background: #eef; } 79 | #TrivialReporter .finished-at { padding-left: 1em; font-size: .6em; } 80 | #TrivialReporter.show-passed .passed, #TrivialReporter.show-skipped .skipped { display: block; } 81 | #TrivialReporter #jasmine_content { position: fixed; right: 100%; } 82 | #TrivialReporter .runner { border: 1px solid gray; display: block; margin: 5px 0; padding: 2px 0 2px 10px; } 83 | -------------------------------------------------------------------------------- /test/performance/artTemplate/artTemplate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Render Time Test - artTemplate 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 32 | 33 | -------------------------------------------------------------------------------- /test/performance/artTemplate/artTemplate.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(/^$|,+/)}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;"function"==typeof define?define(function(){return d}):"undefined"!=typeof exports?module.exports=d:this.template=d}(); -------------------------------------------------------------------------------- /test/performance/artTemplate/template.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/performance/baiduTemplate/baiduTemplate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Render Time Test - baiduTemplate 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 30 | 31 | -------------------------------------------------------------------------------- /test/performance/baiduTemplate/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/performance/baiduTemplate/template.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/performance/compile-time.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ETPL Compile Time Test 6 | 7 | 8 | 9 | 10 | 34 | 35 | -------------------------------------------------------------------------------- /test/performance/dot/dot.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Render Time Test - doT 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 30 | 31 | -------------------------------------------------------------------------------- /test/performance/dot/dot.js: -------------------------------------------------------------------------------- 1 | /* Laura Doktorova https://github.com/olado/doT */ 2 | (function(){function o(){var a={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"},b=/&(?!#?\w+;)|<|>|"|'|\//g;return function(){return this?this.replace(b,function(c){return a[c]||c}):this}}function p(a,b,c){return(typeof b==="string"?b:b.toString()).replace(a.define||i,function(l,e,f,g){if(e.indexOf("def.")===0)e=e.substring(4);if(!(e in c))if(f===":"){a.defineParams&&g.replace(a.defineParams,function(n,h,d){c[e]={arg:h,text:d}});e in c||(c[e]=g)}else(new Function("def","def['"+ 3 | e+"']="+g))(c);return""}).replace(a.use||i,function(l,e){if(a.useParams)e=e.replace(a.useParams,function(g,n,h,d){if(c[h]&&c[h].arg&&d){g=(h+":"+d).replace(/'|\\/g,"_");c.__exp=c.__exp||{};c.__exp[g]=c[h].text.replace(RegExp("(^|[^\\w$])"+c[h].arg+"([^\\w$])","g"),"$1"+d+"$2");return n+"def.__exp['"+g+"']"}});var f=(new Function("def","return "+e))(c);return f?p(a,f,c):f})}function m(a){return a.replace(/\\('|\\)/g,"$1").replace(/[\r\t\n]/g," ")}var j={version:"1.0.1",templateSettings:{evaluate:/\{\{([\s\S]+?(\}?)+)\}\}/g, 4 | interpolate:/\{\{=([\s\S]+?)\}\}/g,encode:/\{\{!([\s\S]+?)\}\}/g,use:/\{\{#([\s\S]+?)\}\}/g,useParams:/(^|[^\w$])def(?:\.|\[[\'\"])([\w$\.]+)(?:[\'\"]\])?\s*\:\s*([\w$\.]+|\"[^\"]+\"|\'[^\']+\'|\{[^\}]+\})/g,define:/\{\{##\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)#\}\}/g,defineParams:/^\s*([\w$]+):([\s\S]+)/,conditional:/\{\{\?(\?)?\s*([\s\S]*?)\s*\}\}/g,iterate:/\{\{~\s*(?:\}\}|([\s\S]+?)\s*\:\s*([\w$]+)\s*(?:\:\s*([\w$]+))?\s*\}\})/g,varname:"it",strip:true,append:true,selfcontained:false},template:undefined, 5 | compile:undefined},q;if(typeof module!=="undefined"&&module.exports)module.exports=j;else if(typeof define==="function"&&define.amd)define(function(){return j});else{q=function(){return this||(0,eval)("this")}();q.doT=j}String.prototype.encodeHTML=o();var r={append:{start:"'+(",end:")+'",endencode:"||'').toString().encodeHTML()+'"},split:{start:"';out+=(",end:");out+='",endencode:"||'').toString().encodeHTML();out+='"}},i=/$^/;j.template=function(a,b,c){b=b||j.templateSettings;var l=b.append?r.append: 6 | r.split,e,f=0,g;a=b.use||b.define?p(b,a,c||{}):a;a=("var out='"+(b.strip?a.replace(/(^|\r|\n)\t* +| +\t*(\r|\n|$)/g," ").replace(/\r|\n|\t|\/\*[\s\S]*?\*\//g,""):a).replace(/'|\\/g,"\\$&").replace(b.interpolate||i,function(h,d){return l.start+m(d)+l.end}).replace(b.encode||i,function(h,d){e=true;return l.start+m(d)+l.endencode}).replace(b.conditional||i,function(h,d,k){return d?k?"';}else if("+m(k)+"){out+='":"';}else{out+='":k?"';if("+m(k)+"){out+='":"';}out+='"}).replace(b.iterate||i,function(h, 7 | d,k,s){if(!d)return"';} } out+='";f+=1;g=s||"i"+f;d=m(d);return"';var arr"+f+"="+d+";if(arr"+f+"){var "+k+","+g+"=-1,l"+f+"=arr"+f+".length-1;while("+g+" 2 | {{~it.persons :person}} 3 |
  • {{!person.name}} [{{!person.email}}] is {{!person.age}} ages.
  • 4 | {{~}} 5 | -------------------------------------------------------------------------------- /test/performance/ejs/ejs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Render Time Test - EJS 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 30 | 31 | -------------------------------------------------------------------------------- /test/performance/ejs/ejs.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | 4 | var rsplit = function(string, regex) { 5 | var result = regex.exec(string),retArr = new Array(), first_idx, last_idx, first_bit; 6 | while (result != null) 7 | { 8 | first_idx = result.index; last_idx = regex.lastIndex; 9 | if ((first_idx) != 0) 10 | { 11 | first_bit = string.substring(0,first_idx); 12 | retArr.push(string.substring(0,first_idx)); 13 | string = string.slice(first_idx); 14 | } 15 | retArr.push(result[0]); 16 | string = string.slice(result[0].length); 17 | result = regex.exec(string); 18 | } 19 | if (! string == '') 20 | { 21 | retArr.push(string); 22 | } 23 | return retArr; 24 | }, 25 | chop = function(string){ 26 | return string.substr(0, string.length - 1); 27 | }, 28 | extend = function(d, s){ 29 | for(var n in s){ 30 | if(s.hasOwnProperty(n)) d[n] = s[n] 31 | } 32 | } 33 | 34 | 35 | EJS = function( options ){ 36 | options = typeof options == "string" ? {view: options} : options 37 | this.set_options(options); 38 | if(options.precompiled){ 39 | this.template = {}; 40 | this.template.process = options.precompiled; 41 | EJS.update(this.name, this); 42 | return; 43 | } 44 | if(options.element) 45 | { 46 | if(typeof options.element == 'string'){ 47 | var name = options.element 48 | options.element = document.getElementById( options.element ) 49 | if(options.element == null) throw name+'does not exist!' 50 | } 51 | if(options.element.value){ 52 | this.text = options.element.value 53 | }else{ 54 | this.text = options.element.innerHTML 55 | } 56 | this.name = options.element.id 57 | this.type = '[' 58 | }else if(options.url){ 59 | options.url = EJS.endExt(options.url, this.extMatch); 60 | this.name = this.name ? this.name : options.url; 61 | var url = options.url 62 | //options.view = options.absolute_url || options.view || options.; 63 | var template = EJS.get(this.name /*url*/, this.cache); 64 | if (template) return template; 65 | if (template == EJS.INVALID_PATH) return null; 66 | try{ 67 | this.text = EJS.request( url+(this.cache ? '' : '?'+Math.random() )); 68 | }catch(e){} 69 | 70 | if(this.text == null){ 71 | throw( {type: 'EJS', message: 'There is no template at '+url} ); 72 | } 73 | //this.name = url; 74 | } 75 | var template = new EJS.Compiler(this.text, this.type); 76 | 77 | template.compile(options, this.name); 78 | 79 | 80 | EJS.update(this.name, this); 81 | this.template = template; 82 | }; 83 | /* @Prototype*/ 84 | EJS.prototype = { 85 | /** 86 | * Renders an object with extra view helpers attached to the view. 87 | * @param {Object} object data to be rendered 88 | * @param {Object} extra_helpers an object with additonal view helpers 89 | * @return {String} returns the result of the string 90 | */ 91 | render : function(object, extra_helpers){ 92 | object = object || {}; 93 | this._extra_helpers = extra_helpers; 94 | var v = new EJS.Helpers(object, extra_helpers || {}); 95 | return this.template.process.call(object, object,v); 96 | }, 97 | update : function(element, options){ 98 | if(typeof element == 'string'){ 99 | element = document.getElementById(element) 100 | } 101 | if(options == null){ 102 | _template = this; 103 | return function(object){ 104 | EJS.prototype.update.call(_template, element, object) 105 | } 106 | } 107 | if(typeof options == 'string'){ 108 | params = {} 109 | params.url = options 110 | _template = this; 111 | params.onComplete = function(request){ 112 | var object = eval( request.responseText ) 113 | EJS.prototype.update.call(_template, element, object) 114 | } 115 | EJS.ajax_request(params) 116 | }else 117 | { 118 | element.innerHTML = this.render(options) 119 | } 120 | }, 121 | out : function(){ 122 | return this.template.out; 123 | }, 124 | /** 125 | * Sets options on this view to be rendered with. 126 | * @param {Object} options 127 | */ 128 | set_options : function(options){ 129 | this.type = options.type || EJS.type; 130 | this.cache = options.cache != null ? options.cache : EJS.cache; 131 | this.text = options.text || null; 132 | this.name = options.name || null; 133 | this.ext = options.ext || EJS.ext; 134 | this.extMatch = new RegExp(this.ext.replace(/\./, '\.')); 135 | } 136 | }; 137 | EJS.endExt = function(path, match){ 138 | if(!path) return null; 139 | match.lastIndex = 0 140 | return path+ (match.test(path) ? '' : this.ext ) 141 | } 142 | 143 | 144 | 145 | 146 | /* @Static*/ 147 | EJS.Scanner = function(source, left, right) { 148 | 149 | extend(this, 150 | {left_delimiter: left +'%', 151 | right_delimiter: '%'+right, 152 | double_left: left+'%%', 153 | double_right: '%%'+right, 154 | left_equal: left+'%=', 155 | left_comment: left+'%#'}) 156 | 157 | this.SplitRegexp = left=='[' ? /(\[%%)|(%%\])|(\[%=)|(\[%#)|(\[%)|(%\]\n)|(%\])|(\n)/ : new RegExp('('+this.double_left+')|(%%'+this.double_right+')|('+this.left_equal+')|('+this.left_comment+')|('+this.left_delimiter+')|('+this.right_delimiter+'\n)|('+this.right_delimiter+')|(\n)') ; 158 | 159 | this.source = source; 160 | this.stag = null; 161 | this.lines = 0; 162 | }; 163 | 164 | EJS.Scanner.to_text = function(input){ 165 | if(input == null || input === undefined) 166 | return ''; 167 | if(input instanceof Date) 168 | return input.toDateString(); 169 | if(input.toString) 170 | return input.toString(); 171 | return ''; 172 | }; 173 | 174 | EJS.Scanner.prototype = { 175 | scan: function(block) { 176 | scanline = this.scanline; 177 | regex = this.SplitRegexp; 178 | if (! this.source == '') 179 | { 180 | var source_split = rsplit(this.source, /\n/); 181 | for(var i=0; i 0) 228 | { 229 | for (var i=0; i 0) 303 | { 304 | buff.push(put_cmd + '"' + clean(content) + '")'); 305 | } 306 | content = ''; 307 | break; 308 | case scanner.double_left: 309 | content = content + scanner.left_delimiter; 310 | break; 311 | default: 312 | content = content + token; 313 | break; 314 | } 315 | } 316 | else { 317 | switch(token) { 318 | case scanner.right_delimiter: 319 | switch(scanner.stag) { 320 | case scanner.left_delimiter: 321 | if (content[content.length - 1] == '\n') 322 | { 323 | content = chop(content); 324 | buff.push(content); 325 | buff.cr(); 326 | } 327 | else { 328 | buff.push(content); 329 | } 330 | break; 331 | case scanner.left_equal: 332 | buff.push(insert_cmd + "(EJS.Scanner.to_text(" + content + ")))"); 333 | break; 334 | } 335 | scanner.stag = null; 336 | content = ''; 337 | break; 338 | case scanner.double_right: 339 | content = content + scanner.right_delimiter; 340 | break; 341 | default: 342 | content = content + token; 343 | break; 344 | } 345 | } 346 | }); 347 | if (content.length > 0) 348 | { 349 | // Chould be content.dump in Ruby 350 | buff.push(put_cmd + '"' + clean(content) + '")'); 351 | } 352 | buff.close(); 353 | this.out = buff.script + ";"; 354 | var to_be_evaled = '/*'+name+'*/this.process = function(_CONTEXT,_VIEW) { try { with(_VIEW) { with (_CONTEXT) {'+this.out+" return ___ViewO.join('');}}}catch(e){e.lineNumber=null;throw e;}};"; 355 | 356 | try{ 357 | eval(to_be_evaled); 358 | }catch(e){ 359 | if(typeof JSLINT != 'undefined'){ 360 | JSLINT(this.out); 361 | for(var i = 0; i < JSLINT.errors.length; i++){ 362 | var error = JSLINT.errors[i]; 363 | if(error.reason != "Unnecessary semicolon."){ 364 | error.line++; 365 | var e = new Error(); 366 | e.lineNumber = error.line; 367 | e.message = error.reason; 368 | if(options.view) 369 | e.fileName = options.view; 370 | throw e; 371 | } 372 | } 373 | }else{ 374 | throw e; 375 | } 376 | } 377 | } 378 | }; 379 | 380 | 381 | //type, cache, folder 382 | /** 383 | * Sets default options for all views 384 | * @param {Object} options Set view with the following options 385 | * 386 | 387 | 388 | 389 | 390 | 392 | 393 | 394 | 395 | 396 | 398 | 399 |
    OptionDefaultDescription
    type'<'type of magic tags. Options are '<' or '[' 391 |
    cachetrue in production mode, false in other modestrue to cache template. 397 |
    400 | * 401 | */ 402 | EJS.config = function(options){ 403 | EJS.cache = options.cache != null ? options.cache : EJS.cache; 404 | EJS.type = options.type != null ? options.type : EJS.type; 405 | EJS.ext = options.ext != null ? options.ext : EJS.ext; 406 | 407 | var templates_directory = EJS.templates_directory || {}; //nice and private container 408 | EJS.templates_directory = templates_directory; 409 | EJS.get = function(path, cache){ 410 | if(cache == false) return null; 411 | if(templates_directory[path]) return templates_directory[path]; 412 | return null; 413 | }; 414 | 415 | EJS.update = function(path, template) { 416 | if(path == null) return; 417 | templates_directory[path] = template ; 418 | }; 419 | 420 | EJS.INVALID_PATH = -1; 421 | }; 422 | EJS.config( {cache: true, type: '<', ext: '.ejs' } ); 423 | 424 | 425 | 426 | /** 427 | * @constructor 428 | * By adding functions to EJS.Helpers.prototype, those functions will be available in the 429 | * views. 430 | * @init Creates a view helper. This function is called internally. You should never call it. 431 | * @param {Object} data The data passed to the view. Helpers have access to it through this._data 432 | */ 433 | EJS.Helpers = function(data, extras){ 434 | this._data = data; 435 | this._extras = extras; 436 | extend(this, extras ); 437 | }; 438 | /* @prototype*/ 439 | EJS.Helpers.prototype = { 440 | /** 441 | * Renders a new view. If data is passed in, uses that to render the view. 442 | * @param {Object} options standard options passed to a new view. 443 | * @param {optional:Object} data 444 | * @return {String} 445 | */ 446 | view: function(options, data, helpers){ 447 | if(!helpers) helpers = this._extras 448 | if(!data) data = this._data; 449 | return new EJS(options).render(data, helpers); 450 | }, 451 | /** 452 | * For a given value, tries to create a human representation. 453 | * @param {Object} input the value being converted. 454 | * @param {Object} null_text what text should be present if input == null or undefined, defaults to '' 455 | * @return {String} 456 | */ 457 | to_text: function(input, null_text) { 458 | if(input == null || input === undefined) return null_text || ''; 459 | if(input instanceof Date) return input.toDateString(); 460 | if(input.toString) return input.toString().replace(/\n/g, '
    ').replace(/''/g, "'"); 461 | return ''; 462 | } 463 | }; 464 | EJS.newRequest = function(){ 465 | var factories = [function() { return new ActiveXObject("Msxml2.XMLHTTP"); },function() { return new XMLHttpRequest(); },function() { return new ActiveXObject("Microsoft.XMLHTTP"); }]; 466 | for(var i = 0; i < factories.length; i++) { 467 | try { 468 | var request = factories[i](); 469 | if (request != null) return request; 470 | } 471 | catch(e) { continue;} 472 | } 473 | } 474 | 475 | EJS.request = function(path){ 476 | var request = new EJS.newRequest() 477 | request.open("GET", path, false); 478 | 479 | try{request.send(null);} 480 | catch(e){return null;} 481 | 482 | if ( request.status == 404 || request.status == 2 ||(request.status == 0 && request.responseText == '') ) return null; 483 | 484 | return request.responseText 485 | } 486 | EJS.ajax_request = function(params){ 487 | params.method = ( params.method ? params.method : 'GET') 488 | 489 | var request = new EJS.newRequest(); 490 | request.onreadystatechange = function(){ 491 | if(request.readyState == 4){ 492 | if(request.status == 200){ 493 | params.onComplete(request) 494 | }else 495 | { 496 | params.onComplete(request) 497 | } 498 | } 499 | } 500 | request.open(params.method, params.url) 501 | request.send(null) 502 | } 503 | 504 | 505 | })(); -------------------------------------------------------------------------------- /test/performance/ejs/template.html: -------------------------------------------------------------------------------- 1 |
      2 | <% for(var i = 0, len = persons.length; i < len; i++) {%> 3 |
    • <%= persons[i].name %> [<%= persons[i].email %>] is <%= persons[i].age %> ages.
    • 4 | <% } %> 5 |
    -------------------------------------------------------------------------------- /test/performance/etpl/etpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Render Time Test - ETPL 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 30 | 31 | -------------------------------------------------------------------------------- /test/performance/etpl/template.html: -------------------------------------------------------------------------------- 1 |
      2 | 3 |
    • ${person.name} [${person.email}] is ${person.age} ages.
    • 4 | 5 |
    -------------------------------------------------------------------------------- /test/performance/handlebars/handlebars.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Render Time Test - mustache 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 30 | 31 | -------------------------------------------------------------------------------- /test/performance/handlebars/template.html: -------------------------------------------------------------------------------- 1 |
      2 | {{#persons}} 3 |
    • {{name}} [{{email}}] is {{age}} ages.
    • 4 | {{/persons}} 5 |
    -------------------------------------------------------------------------------- /test/performance/hogan/hogan-2.0.0.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 Twitter, Inc. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | 17 | 18 | var Hogan = {}; 19 | 20 | (function (Hogan, useArrayBuffer) { 21 | Hogan.Template = function (renderFunc, text, compiler, options) { 22 | this.r = renderFunc || this.r; 23 | this.c = compiler; 24 | this.options = options; 25 | this.text = text || ''; 26 | this.buf = (useArrayBuffer) ? [] : ''; 27 | } 28 | 29 | Hogan.Template.prototype = { 30 | // render: replaced by generated code. 31 | r: function (context, partials, indent) { return ''; }, 32 | 33 | // variable escaping 34 | v: hoganEscape, 35 | 36 | // triple stache 37 | t: coerceToString, 38 | 39 | render: function render(context, partials, indent) { 40 | return this.ri([context], partials || {}, indent); 41 | }, 42 | 43 | // render internal -- a hook for overrides that catches partials too 44 | ri: function (context, partials, indent) { 45 | return this.r(context, partials, indent); 46 | }, 47 | 48 | // tries to find a partial in the curent scope and render it 49 | rp: function(name, context, partials, indent) { 50 | var partial = partials[name]; 51 | 52 | if (!partial) { 53 | return ''; 54 | } 55 | 56 | if (this.c && typeof partial == 'string') { 57 | partial = this.c.compile(partial, this.options); 58 | } 59 | 60 | return partial.ri(context, partials, indent); 61 | }, 62 | 63 | // render a section 64 | rs: function(context, partials, section) { 65 | var tail = context[context.length - 1]; 66 | 67 | if (!isArray(tail)) { 68 | section(context, partials, this); 69 | return; 70 | } 71 | 72 | for (var i = 0; i < tail.length; i++) { 73 | context.push(tail[i]); 74 | section(context, partials, this); 75 | context.pop(); 76 | } 77 | }, 78 | 79 | // maybe start a section 80 | s: function(val, ctx, partials, inverted, start, end, tags) { 81 | var pass; 82 | 83 | if (isArray(val) && val.length === 0) { 84 | return false; 85 | } 86 | 87 | if (typeof val == 'function') { 88 | val = this.ls(val, ctx, partials, inverted, start, end, tags); 89 | } 90 | 91 | pass = (val === '') || !!val; 92 | 93 | if (!inverted && pass && ctx) { 94 | ctx.push((typeof val == 'object') ? val : ctx[ctx.length - 1]); 95 | } 96 | 97 | return pass; 98 | }, 99 | 100 | // find values with dotted names 101 | d: function(key, ctx, partials, returnFound) { 102 | var names = key.split('.'), 103 | val = this.f(names[0], ctx, partials, returnFound), 104 | cx = null; 105 | 106 | if (key === '.' && isArray(ctx[ctx.length - 2])) { 107 | return ctx[ctx.length - 1]; 108 | } 109 | 110 | for (var i = 1; i < names.length; i++) { 111 | if (val && typeof val == 'object' && names[i] in val) { 112 | cx = val; 113 | val = val[names[i]]; 114 | } else { 115 | val = ''; 116 | } 117 | } 118 | 119 | if (returnFound && !val) { 120 | return false; 121 | } 122 | 123 | if (!returnFound && typeof val == 'function') { 124 | ctx.push(cx); 125 | val = this.lv(val, ctx, partials); 126 | ctx.pop(); 127 | } 128 | 129 | return val; 130 | }, 131 | 132 | // find values with normal names 133 | f: function(key, ctx, partials, returnFound) { 134 | var val = false, 135 | v = null, 136 | found = false; 137 | 138 | for (var i = ctx.length - 1; i >= 0; i--) { 139 | v = ctx[i]; 140 | if (v && typeof v == 'object' && key in v) { 141 | val = v[key]; 142 | found = true; 143 | break; 144 | } 145 | } 146 | 147 | if (!found) { 148 | return (returnFound) ? false : ""; 149 | } 150 | 151 | if (!returnFound && typeof val == 'function') { 152 | val = this.lv(val, ctx, partials); 153 | } 154 | 155 | return val; 156 | }, 157 | 158 | // higher order templates 159 | ho: function(val, cx, partials, text, tags) { 160 | var compiler = this.c; 161 | var options = this.options; 162 | options.delimiters = tags; 163 | var text = val.call(cx, text); 164 | text = (text == null) ? String(text) : text.toString(); 165 | this.b(compiler.compile(text, options).render(cx, partials)); 166 | return false; 167 | }, 168 | 169 | // template result buffering 170 | b: (useArrayBuffer) ? function(s) { this.buf.push(s); } : 171 | function(s) { this.buf += s; }, 172 | fl: (useArrayBuffer) ? function() { var r = this.buf.join(''); this.buf = []; return r; } : 173 | function() { var r = this.buf; this.buf = ''; return r; }, 174 | 175 | // lambda replace section 176 | ls: function(val, ctx, partials, inverted, start, end, tags) { 177 | var cx = ctx[ctx.length - 1], 178 | t = null; 179 | 180 | if (!inverted && this.c && val.length > 0) { 181 | return this.ho(val, cx, partials, this.text.substring(start, end), tags); 182 | } 183 | 184 | t = val.call(cx); 185 | 186 | if (typeof t == 'function') { 187 | if (inverted) { 188 | return true; 189 | } else if (this.c) { 190 | return this.ho(t, cx, partials, this.text.substring(start, end), tags); 191 | } 192 | } 193 | 194 | return t; 195 | }, 196 | 197 | // lambda replace variable 198 | lv: function(val, ctx, partials) { 199 | var cx = ctx[ctx.length - 1]; 200 | var result = val.call(cx); 201 | 202 | if (typeof result == 'function') { 203 | result = coerceToString(result.call(cx)); 204 | if (this.c && ~result.indexOf("{\u007B")) { 205 | return this.c.compile(result, this.options).render(cx, partials); 206 | } 207 | } 208 | 209 | return coerceToString(result); 210 | } 211 | 212 | }; 213 | 214 | var rAmp = /&/g, 215 | rLt = //g, 217 | rApos =/\'/g, 218 | rQuot = /\"/g, 219 | hChars =/[&<>\"\']/; 220 | 221 | 222 | function coerceToString(val) { 223 | return String((val === null || val === undefined) ? '' : val); 224 | } 225 | 226 | function hoganEscape(str) { 227 | str = coerceToString(str); 228 | return hChars.test(str) ? 229 | str 230 | .replace(rAmp,'&') 231 | .replace(rLt,'<') 232 | .replace(rGt,'>') 233 | .replace(rApos,''') 234 | .replace(rQuot, '"') : 235 | str; 236 | } 237 | 238 | var isArray = Array.isArray || function(a) { 239 | return Object.prototype.toString.call(a) === '[object Array]'; 240 | }; 241 | 242 | })(typeof exports !== 'undefined' ? exports : Hogan); 243 | 244 | 245 | 246 | 247 | (function (Hogan) { 248 | // Setup regex assignments 249 | // remove whitespace according to Mustache spec 250 | var rIsWhitespace = /\S/, 251 | rQuot = /\"/g, 252 | rNewline = /\n/g, 253 | rCr = /\r/g, 254 | rSlash = /\\/g, 255 | tagTypes = { 256 | '#': 1, '^': 2, '/': 3, '!': 4, '>': 5, 257 | '<': 6, '=': 7, '_v': 8, '{': 9, '&': 10 258 | }; 259 | 260 | Hogan.scan = function scan(text, delimiters) { 261 | var len = text.length, 262 | IN_TEXT = 0, 263 | IN_TAG_TYPE = 1, 264 | IN_TAG = 2, 265 | state = IN_TEXT, 266 | tagType = null, 267 | tag = null, 268 | buf = '', 269 | tokens = [], 270 | seenTag = false, 271 | i = 0, 272 | lineStart = 0, 273 | otag = '{{', 274 | ctag = '}}'; 275 | 276 | function addBuf() { 277 | if (buf.length > 0) { 278 | tokens.push(new String(buf)); 279 | buf = ''; 280 | } 281 | } 282 | 283 | function lineIsWhitespace() { 284 | var isAllWhitespace = true; 285 | for (var j = lineStart; j < tokens.length; j++) { 286 | isAllWhitespace = 287 | (tokens[j].tag && tagTypes[tokens[j].tag] < tagTypes['_v']) || 288 | (!tokens[j].tag && tokens[j].match(rIsWhitespace) === null); 289 | if (!isAllWhitespace) { 290 | return false; 291 | } 292 | } 293 | 294 | return isAllWhitespace; 295 | } 296 | 297 | function filterLine(haveSeenTag, noNewLine) { 298 | addBuf(); 299 | 300 | if (haveSeenTag && lineIsWhitespace()) { 301 | for (var j = lineStart, next; j < tokens.length; j++) { 302 | if (!tokens[j].tag) { 303 | if ((next = tokens[j+1]) && next.tag == '>') { 304 | // set indent to token value 305 | next.indent = tokens[j].toString() 306 | } 307 | tokens.splice(j, 1); 308 | } 309 | } 310 | } else if (!noNewLine) { 311 | tokens.push({tag:'\n'}); 312 | } 313 | 314 | seenTag = false; 315 | lineStart = tokens.length; 316 | } 317 | 318 | function changeDelimiters(text, index) { 319 | var close = '=' + ctag, 320 | closeIndex = text.indexOf(close, index), 321 | delimiters = trim( 322 | text.substring(text.indexOf('=', index) + 1, closeIndex) 323 | ).split(' '); 324 | 325 | otag = delimiters[0]; 326 | ctag = delimiters[1]; 327 | 328 | return closeIndex + close.length - 1; 329 | } 330 | 331 | if (delimiters) { 332 | delimiters = delimiters.split(' '); 333 | otag = delimiters[0]; 334 | ctag = delimiters[1]; 335 | } 336 | 337 | for (i = 0; i < len; i++) { 338 | if (state == IN_TEXT) { 339 | if (tagChange(otag, text, i)) { 340 | --i; 341 | addBuf(); 342 | state = IN_TAG_TYPE; 343 | } else { 344 | if (text.charAt(i) == '\n') { 345 | filterLine(seenTag); 346 | } else { 347 | buf += text.charAt(i); 348 | } 349 | } 350 | } else if (state == IN_TAG_TYPE) { 351 | i += otag.length - 1; 352 | tag = tagTypes[text.charAt(i + 1)]; 353 | tagType = tag ? text.charAt(i + 1) : '_v'; 354 | if (tagType == '=') { 355 | i = changeDelimiters(text, i); 356 | state = IN_TEXT; 357 | } else { 358 | if (tag) { 359 | i++; 360 | } 361 | state = IN_TAG; 362 | } 363 | seenTag = i; 364 | } else { 365 | if (tagChange(ctag, text, i)) { 366 | tokens.push({tag: tagType, n: trim(buf), otag: otag, ctag: ctag, 367 | i: (tagType == '/') ? seenTag - ctag.length : i + otag.length}); 368 | buf = ''; 369 | i += ctag.length - 1; 370 | state = IN_TEXT; 371 | if (tagType == '{') { 372 | if (ctag == '}}') { 373 | i++; 374 | } else { 375 | cleanTripleStache(tokens[tokens.length - 1]); 376 | } 377 | } 378 | } else { 379 | buf += text.charAt(i); 380 | } 381 | } 382 | } 383 | 384 | filterLine(seenTag, true); 385 | 386 | return tokens; 387 | } 388 | 389 | function cleanTripleStache(token) { 390 | if (token.n.substr(token.n.length - 1) === '}') { 391 | token.n = token.n.substring(0, token.n.length - 1); 392 | } 393 | } 394 | 395 | function trim(s) { 396 | if (s.trim) { 397 | return s.trim(); 398 | } 399 | 400 | return s.replace(/^\s*|\s*$/g, ''); 401 | } 402 | 403 | function tagChange(tag, text, index) { 404 | if (text.charAt(index) != tag.charAt(0)) { 405 | return false; 406 | } 407 | 408 | for (var i = 1, l = tag.length; i < l; i++) { 409 | if (text.charAt(index + i) != tag.charAt(i)) { 410 | return false; 411 | } 412 | } 413 | 414 | return true; 415 | } 416 | 417 | function buildTree(tokens, kind, stack, customTags) { 418 | var instructions = [], 419 | opener = null, 420 | token = null; 421 | 422 | while (tokens.length > 0) { 423 | token = tokens.shift(); 424 | if (token.tag == '#' || token.tag == '^' || isOpener(token, customTags)) { 425 | stack.push(token); 426 | token.nodes = buildTree(tokens, token.tag, stack, customTags); 427 | instructions.push(token); 428 | } else if (token.tag == '/') { 429 | if (stack.length === 0) { 430 | throw new Error('Closing tag without opener: /' + token.n); 431 | } 432 | opener = stack.pop(); 433 | if (token.n != opener.n && !isCloser(token.n, opener.n, customTags)) { 434 | throw new Error('Nesting error: ' + opener.n + ' vs. ' + token.n); 435 | } 436 | opener.end = token.i; 437 | return instructions; 438 | } else { 439 | instructions.push(token); 440 | } 441 | } 442 | 443 | if (stack.length > 0) { 444 | throw new Error('missing closing tag: ' + stack.pop().n); 445 | } 446 | 447 | return instructions; 448 | } 449 | 450 | function isOpener(token, tags) { 451 | for (var i = 0, l = tags.length; i < l; i++) { 452 | if (tags[i].o == token.n) { 453 | token.tag = '#'; 454 | return true; 455 | } 456 | } 457 | } 458 | 459 | function isCloser(close, open, tags) { 460 | for (var i = 0, l = tags.length; i < l; i++) { 461 | if (tags[i].c == close && tags[i].o == open) { 462 | return true; 463 | } 464 | } 465 | } 466 | 467 | Hogan.generate = function (tree, text, options) { 468 | var code = 'var _=this;_.b(i=i||"");' + walk(tree) + 'return _.fl();'; 469 | if (options.asString) { 470 | return 'function(c,p,i){' + code + ';}'; 471 | } 472 | 473 | return new Hogan.Template(new Function('c', 'p', 'i', code), text, Hogan, options); 474 | } 475 | 476 | function esc(s) { 477 | return s.replace(rSlash, '\\\\') 478 | .replace(rQuot, '\\\"') 479 | .replace(rNewline, '\\n') 480 | .replace(rCr, '\\r'); 481 | } 482 | 483 | function chooseMethod(s) { 484 | return (~s.indexOf('.')) ? 'd' : 'f'; 485 | } 486 | 487 | function walk(tree) { 488 | var code = ''; 489 | for (var i = 0, l = tree.length; i < l; i++) { 490 | var tag = tree[i].tag; 491 | if (tag == '#') { 492 | code += section(tree[i].nodes, tree[i].n, chooseMethod(tree[i].n), 493 | tree[i].i, tree[i].end, tree[i].otag + " " + tree[i].ctag); 494 | } else if (tag == '^') { 495 | code += invertedSection(tree[i].nodes, tree[i].n, 496 | chooseMethod(tree[i].n)); 497 | } else if (tag == '<' || tag == '>') { 498 | code += partial(tree[i]); 499 | } else if (tag == '{' || tag == '&') { 500 | code += tripleStache(tree[i].n, chooseMethod(tree[i].n)); 501 | } else if (tag == '\n') { 502 | code += text('"\\n"' + (tree.length-1 == i ? '' : ' + i')); 503 | } else if (tag == '_v') { 504 | code += variable(tree[i].n, chooseMethod(tree[i].n)); 505 | } else if (tag === undefined) { 506 | code += text('"' + esc(tree[i]) + '"'); 507 | } 508 | } 509 | return code; 510 | } 511 | 512 | function section(nodes, id, method, start, end, tags) { 513 | return 'if(_.s(_.' + method + '("' + esc(id) + '",c,p,1),' + 514 | 'c,p,0,' + start + ',' + end + ',"' + tags + '")){' + 515 | '_.rs(c,p,' + 516 | 'function(c,p,_){' + 517 | walk(nodes) + 518 | '});c.pop();}'; 519 | } 520 | 521 | function invertedSection(nodes, id, method) { 522 | return 'if(!_.s(_.' + method + '("' + esc(id) + '",c,p,1),c,p,1,0,0,"")){' + 523 | walk(nodes) + 524 | '};'; 525 | } 526 | 527 | function partial(tok) { 528 | return '_.b(_.rp("' + esc(tok.n) + '",c,p,"' + (tok.indent || '') + '"));'; 529 | } 530 | 531 | function tripleStache(id, method) { 532 | return '_.b(_.t(_.' + method + '("' + esc(id) + '",c,p,0)));'; 533 | } 534 | 535 | function variable(id, method) { 536 | return '_.b(_.v(_.' + method + '("' + esc(id) + '",c,p,0)));'; 537 | } 538 | 539 | function text(id) { 540 | return '_.b(' + id + ');'; 541 | } 542 | 543 | Hogan.parse = function(tokens, text, options) { 544 | options = options || {}; 545 | return buildTree(tokens, '', [], options.sectionTags || []); 546 | }, 547 | 548 | Hogan.cache = {}; 549 | 550 | Hogan.compile = function(text, options) { 551 | // options 552 | // 553 | // asString: false (default) 554 | // 555 | // sectionTags: [{o: '_foo', c: 'foo'}] 556 | // An array of object with o and c fields that indicate names for custom 557 | // section tags. The example above allows parsing of {{_foo}}{{/foo}}. 558 | // 559 | // delimiters: A string that overrides the default delimiters. 560 | // Example: "<% %>" 561 | // 562 | options = options || {}; 563 | 564 | var key = text + '||' + !!options.asString; 565 | 566 | var t = this.cache[key]; 567 | 568 | if (t) { 569 | return t; 570 | } 571 | 572 | t = this.generate(this.parse(this.scan(text, options.delimiters), text, options), text, options); 573 | return this.cache[key] = t; 574 | }; 575 | })(typeof exports !== 'undefined' ? exports : Hogan); 576 | 577 | -------------------------------------------------------------------------------- /test/performance/hogan/hogan.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Render Time Test - mustache 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 30 | 31 | -------------------------------------------------------------------------------- /test/performance/hogan/template.html: -------------------------------------------------------------------------------- 1 |
      2 | {{#persons}} 3 |
    • {{name}} [{{email}}] is {{age}} ages.
    • 4 | {{/persons}} 5 |
    -------------------------------------------------------------------------------- /test/performance/juicer/juicer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Render Time Test - juicer 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 30 | 31 | -------------------------------------------------------------------------------- /test/performance/juicer/juicer.js: -------------------------------------------------------------------------------- 1 | (function(){var c=function(){var e=[].slice.call(arguments);e.push(c.options);if(e[0].match(/^\s*#([\w:\-\.]+)\s*$/igm)){e[0].replace(/^\s*#([\w:\-\.]+)\s*$/igm,function(h,i){var f=document;var g=f&&f.getElementById(i);e[0]=g?(g.value||g.innerHTML):h;});}if(arguments.length==1){return c.compile.apply(c,e);}if(arguments.length>=2){return c.to_html.apply(c,e);}};var d={escapehash:{"<":"<",">":">","&":"&",'"':""","'":"'","/":"/"},escapereplace:function(e){return d.escapehash[e];},escaping:function(e){return typeof(e)!=="string"?e:e.replace(/[&<>"]/igm,this.escapereplace);},detection:function(e){return typeof(e)==="undefined"?"":e;}};var b=function(e){if(typeof(console)!=="undefined"){if(console.warn){console.warn(e);return;}if(console.log){console.log(e);return;}}throw (e);};var a=function(h,f){h=h!==Object(h)?{}:h;if(h.__proto__){h.__proto__=f;return h;}var g=function(){};var j=Object.create?Object.create(f):new (g.prototype=f,g);for(var e in h){if(h.hasOwnProperty(e)){j[e]=h[e];}}return j;};c.__cache={};c.version="0.6.5-stable";c.settings={};c.tags={operationOpen:"{@",operationClose:"}",interpolateOpen:"\\${",interpolateClose:"}",noneencodeOpen:"\\$\\${",noneencodeClose:"}",commentOpen:"\\{#",commentClose:"\\}"};c.options={cache:true,strip:true,errorhandling:true,detection:true,_method:a({__escapehtml:d,__throw:b,__juicer:c},{})};c.tagInit=function(){var f=c.tags.operationOpen+"each\\s*([^}]*?)\\s*as\\s*(\\w*?)\\s*(,\\s*\\w*?)?"+c.tags.operationClose;var h=c.tags.operationOpen+"\\/each"+c.tags.operationClose;var i=c.tags.operationOpen+"if\\s*([^}]*?)"+c.tags.operationClose;var j=c.tags.operationOpen+"\\/if"+c.tags.operationClose;var n=c.tags.operationOpen+"else"+c.tags.operationClose;var o=c.tags.operationOpen+"else if\\s*([^}]*?)"+c.tags.operationClose;var k=c.tags.interpolateOpen+"([\\s\\S]+?)"+c.tags.interpolateClose;var l=c.tags.noneencodeOpen+"([\\s\\S]+?)"+c.tags.noneencodeClose;var m=c.tags.commentOpen+"[^}]*?"+c.tags.commentClose;var g=c.tags.operationOpen+"each\\s*(\\w*?)\\s*in\\s*range\\(([^}]+?)\\s*,\\s*([^}]+?)\\)"+c.tags.operationClose;var e=c.tags.operationOpen+"include\\s*([^}]*?)\\s*,\\s*([^}]*?)"+c.tags.operationClose;c.settings.forstart=new RegExp(f,"igm");c.settings.forend=new RegExp(h,"igm");c.settings.ifstart=new RegExp(i,"igm");c.settings.ifend=new RegExp(j,"igm");c.settings.elsestart=new RegExp(n,"igm");c.settings.elseifstart=new RegExp(o,"igm");c.settings.interpolate=new RegExp(k,"igm");c.settings.noneencode=new RegExp(l,"igm");c.settings.inlinecomment=new RegExp(m,"igm");c.settings.rangestart=new RegExp(g,"igm");c.settings.include=new RegExp(e,"igm");};c.tagInit();c.set=function(f,j){var h=this;var e=function(i){return i.replace(/[\$\(\)\[\]\+\^\{\}\?\*\|\.]/igm,function(l){return"\\"+l;});};var k=function(l,m){var i=l.match(/^tag::(.*)$/i);if(i){h.tags[i[1]]=e(m);h.tagInit();return;}h.options[l]=m;};if(arguments.length===2){k(f,j);return;}if(f===Object(f)){for(var g in f){if(f.hasOwnProperty(g)){k(g,f[g]);}}}};c.register=function(g,f){var e=this.options._method;if(e.hasOwnProperty(g)){return false;}return e[g]=f;};c.unregister=function(f){var e=this.options._method;if(e.hasOwnProperty(f)){return delete e[f];}};c.template=function(e){var f=this;this.options=e;this.__interpolate=function(g,l,i){var h=g.split("|"),k=h[0]||"",j;if(h.length>1){g=h.shift();j=h.shift().split(",");k="_method."+j.shift()+".call({}, "+[g].concat(j)+")";}return"<%= "+(l?"_method.__escapehtml.escaping":"")+"("+(!i||i.detection!==false?"_method.__escapehtml.detection":"")+"("+k+")) %>";};this.__removeShell=function(h,g){var i=0;h=h.replace(c.settings.forstart,function(n,k,m,l){var m=m||"value",l=l&&l.substr(1);var j="i"+i++;return"<% ~function() {for(var "+j+" in "+k+") {if("+k+".hasOwnProperty("+j+")) {var "+m+"="+k+"["+j+"];"+(l?("var "+l+"="+j+";"):"")+" %>";}).replace(c.settings.forend,"<% }}}(); %>").replace(c.settings.ifstart,function(j,k){return"<% if("+k+") { %>";}).replace(c.settings.ifend,"<% } %>").replace(c.settings.elsestart,function(j){return"<% } else { %>";}).replace(c.settings.elseifstart,function(j,k){return"<% } else if("+k+") { %>";}).replace(c.settings.noneencode,function(k,j){return f.__interpolate(j,false,g);}).replace(c.settings.interpolate,function(k,j){return f.__interpolate(j,true,g);}).replace(c.settings.inlinecomment,"").replace(c.settings.rangestart,function(m,l,n,k){var j="j"+i++;return"<% ~function() {for(var "+j+"="+n+";"+j+"<"+k+";"+j+"++) {{var "+l+"="+j+"; %>";}).replace(c.settings.include,function(l,j,k){return"<%= _method.__juicer("+j+", "+k+"); %>";});if(!g||g.errorhandling!==false){h="<% try { %>"+h;h+='<% } catch(e) {_method.__throw("Juicer Render Exception: "+e.message);} %>';}return h;};this.__toNative=function(h,g){return this.__convert(h,!g||g.strip);};this.__lexicalAnalyze=function(k){var j=[];var o=[];var n="";var g=["if","each","_","_method","console","break","case","catch","continue","debugger","default","delete","do","finally","for","function","in","instanceof","new","return","switch","this","throw","try","typeof","var","void","while","with","null","typeof","class","enum","export","extends","import","super","implements","interface","let","package","private","protected","public","static","yield","const","arguments","true","false","undefined","NaN"];var m=function(r,q){if(Array.prototype.indexOf&&r.indexOf===Array.prototype.indexOf){return r.indexOf(q);}for(var p=0;p=,\(\)\[\]]\s*([A-Za-z_]+)/igm,h);for(var l=0;l";};this.__convert=function(h,i){var g=[].join("");g+="'use strict';";g+="var _=_||{};";g+="var _out='';_out+='";if(i!==false){g+=h.replace(/\\/g,"\\\\").replace(/[\r\t\n]/g," ").replace(/'(?=[^%]*%>)/g,"\t").split("'").join("\\'").split("\t").join("'").replace(/<%=(.+?)%>/g,"';_out+=$1;_out+='").split("<%").join("';").split("%>").join("_out+='")+"';return _out;";return g;}g+=h.replace(/\\/g,"\\\\").replace(/[\r]/g,"\\r").replace(/[\t]/g,"\\t").replace(/[\n]/g,"\\n").replace(/'(?=[^%]*%>)/g,"\t").split("'").join("\\'").split("\t").join("'").replace(/<%=(.+?)%>/g,"';_out+=$1;_out+='").split("<%").join("';").split("%>").join("_out+='")+"';return _out.replace(/[\\r\\n]\\s+[\\r\\n]/g, '\\r\\n');";return g;};this.parse=function(h,g){var i=this;if(!g||g.loose!==false){h=this.__lexicalAnalyze(h)+h;}h=this.__removeShell(h,g);h=this.__toNative(h,g);this._render=new Function("_, _method",h);this.render=function(k,j){if(!j||j!==f.options._method){j=a(j,f.options._method);}return i._render.call(this,k,j);};return this;};};c.compile=function(g,f){if(!f||f!==this.options){f=a(f,this.options);}try{var h=this.__cache[g]?this.__cache[g]:new this.template(this.options).parse(g,f);if(!f||f.cache!==false){this.__cache[g]=h;}return h;}catch(i){b("Juicer Compile Exception: "+i.message);return{render:function(){}};}};c.to_html=function(f,g,e){if(!e||e!==this.options){e=a(e,this.options);}return this.compile(f,e).render(g,e._method);};typeof(module)!=="undefined"&&module.exports?module.exports=c:this.juicer=c;})(); -------------------------------------------------------------------------------- /test/performance/juicer/template.html: -------------------------------------------------------------------------------- 1 |
      2 | {@each persons as person} 3 |
    • ${person.name} [${person.email}] is ${person.age} ages.
    • 4 | {@/each} 5 |
    -------------------------------------------------------------------------------- /test/performance/load-template.js: -------------------------------------------------------------------------------- 1 | function loadTemplate( url ) { 2 | var xhr = window.XMLHttpRequest 3 | ? new XMLHttpRequest() 4 | : new ActiveXObject( 'Microsoft.XMLHTTP' ); 5 | xhr.open( 'GET', url, false ); 6 | xhr.send( null ); 7 | 8 | if ( xhr.status >= 200 && xhr.status < 300 ) { 9 | return xhr.responseText; 10 | } 11 | 12 | return ''; 13 | } -------------------------------------------------------------------------------- /test/performance/loops.js: -------------------------------------------------------------------------------- 1 | var loops = 100; 2 | 3 | if ( /^\?([0-9]+)\*([0-9]+)/.test(location.search) ) { 4 | loops = parseInt( RegExp.$2, 10 ); 5 | } -------------------------------------------------------------------------------- /test/performance/mustache/mustache.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Render Time Test - mustache 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 30 | 31 | -------------------------------------------------------------------------------- /test/performance/mustache/mustache.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * mustache.js - Logic-less {{mustache}} templates with JavaScript 3 | * http://github.com/janl/mustache.js 4 | */ 5 | 6 | /*global define: false*/ 7 | 8 | (function (root, factory) { 9 | if (typeof exports === "object" && exports) { 10 | factory(exports); // CommonJS 11 | } else { 12 | var mustache = {}; 13 | factory(mustache); 14 | if (typeof define === "function" && define.amd) { 15 | define(mustache); // AMD 16 | } else { 17 | root.Mustache = mustache; // 6 | 7 | 8 | 9 | 10 | 11 | 12 | 31 | 32 | -------------------------------------------------------------------------------- /test/performance/nunjucks/template.html: -------------------------------------------------------------------------------- 1 |
      2 | {% for person in persons %} 3 |
    • {{person.name|escape}} [{{person.email|escape}}] is {{person.age|escape}} ages.
    • 4 | {% endfor %} 5 |
    -------------------------------------------------------------------------------- /test/performance/render-data.js: -------------------------------------------------------------------------------- 1 | var data = { persons: [] }; 2 | (function () { 3 | var len = 10000; 4 | if ( /^\?([0-9]+)\*([0-9]+)/.test(location.search) ) { 5 | len = parseInt( RegExp.$1, 10 ); 6 | } 7 | 8 | for ( var i = 0; i < len; i++ ) { 9 | data.persons.push( { 10 | name: ' 2 | 3 | 4 | 5 | ETPL Render Time Test 6 | 16 | 91 | 92 | 93 | 94 | 95 | 96 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /test/run.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ETPL Spec Runner - use jasmine-1.3.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | 3 | 4 | var CONTENT_TYPE = { 5 | html: 'text/html', 6 | js: 'text/javascript', 7 | css: 'text/css', 8 | gif: 'image/gif', 9 | jpg: 'image/jpeg', 10 | png: 'image/png' 11 | }; 12 | 13 | var path = require('path'); 14 | var fs = require('fs'); 15 | 16 | http.createServer( 17 | function (request, response) { 18 | var url = (request.url.replace('/','') || 'index.html').replace(/\?.*$/, ''); 19 | var contentType = url.slice( url.lastIndexOf('.') + 1 ); 20 | 21 | var resFile = path.resolve( __dirname, url ); 22 | if ( fs.existsSync( resFile ) ) { 23 | response.writeHead( 200, {'Content-Type': CONTENT_TYPE[ contentType ] } ); 24 | response.write( fs.readFileSync( resFile ) ); 25 | response.end(); 26 | } 27 | else { 28 | response.statusCode = 404; 29 | response.end(); 30 | } 31 | } 32 | ).listen(9999); 33 | 34 | console.log('http://localhost:9999/run.html'); 35 | -------------------------------------------------------------------------------- /test/spec/amdplugin-absolute.text.html: -------------------------------------------------------------------------------- 1 | Hello AMD Plugin from absolute! -------------------------------------------------------------------------------- /test/spec/amdplugin-relative.text.html: -------------------------------------------------------------------------------- 1 | Hello AMD Plugin from relative! -------------------------------------------------------------------------------- /test/spec/amdplugin.js: -------------------------------------------------------------------------------- 1 | define( 2 | function (require) { 3 | var tplSource = require( 'etpl/tpl!spec/amdplugin.text.html' ); 4 | var tplSourceRelative = require( 'etpl/tpl!./amdplugin-relative.text.html' ); 5 | var tplSourceAbsolute = require( 'etpl/tpl!/spec/amdplugin-absolute.text.html' ); 6 | var etpl = require( 'etpl' ); 7 | 8 | describe('AMD Plugin', function() { 9 | it( 10 | 'load template file and auto compile success', 11 | function() { 12 | expect(etpl.render('amdpluginTarget')) 13 | .toEqual('Hello AMD Plugin!'); 14 | expect(tplSource) 15 | .toEqual('Hello AMD Plugin!'); 16 | } 17 | ); 18 | 19 | it( 20 | 'load template file and auto compile success when resource id use relative id', 21 | function() { 22 | expect(etpl.render('amdpluginRelativeTarget')) 23 | .toEqual('Hello AMD Plugin from relative!'); 24 | expect(tplSourceRelative) 25 | .toEqual('Hello AMD Plugin from relative!'); 26 | } 27 | ); 28 | 29 | it( 30 | 'load template file and auto compile success when resource id use absolute path', 31 | function() { 32 | expect(etpl.render('amdpluginAbsoluteTarget')) 33 | .toEqual('Hello AMD Plugin from absolute!'); 34 | expect(tplSourceAbsolute) 35 | .toEqual('Hello AMD Plugin from absolute!'); 36 | } 37 | ); 38 | 39 | 40 | }); 41 | } 42 | ); -------------------------------------------------------------------------------- /test/spec/amdplugin.text.html: -------------------------------------------------------------------------------- 1 | Hello AMD Plugin! -------------------------------------------------------------------------------- /test/spec/comment.spec.js: -------------------------------------------------------------------------------- 1 | (function (){ 2 | 3 | var etpl = require( '../../src/main' ); 4 | var readText = require( './readTextSync' ); 5 | var text = readText( 'comment.text.html' ); 6 | 7 | describe('Comment', function() { 8 | it('should not included on render', function() { 9 | var renderer = etpl.compile( text['tpl-comment'] ); 10 | expect(renderer()).toEqual(text['expect-comment']); 11 | }); 12 | 13 | it('start not matched should be as normal text', function() { 14 | var renderer = etpl.compile( text['tpl-simple'] ); 15 | expect(renderer()).toEqual(text['expect-simple']); 16 | }); 17 | }); 18 | 19 | })(); -------------------------------------------------------------------------------- /test/spec/comment.text.html: -------------------------------------------------------------------------------- 1 | ::tpl-simple:: 2 | 3 | 4 | Hello etpl! 5 | ::expect-simple:: 6 | 7 | 8 | Hello etpl! 9 | ::tpl-comment:: 10 | 11 | 12 | Hello etpl! 13 | ::expect-comment:: 14 | 15 | 16 | Hello etpl! 17 | ::eof:: -------------------------------------------------------------------------------- /test/spec/engine.spec.js: -------------------------------------------------------------------------------- 1 | var engineLog = (function () { 2 | var content; 3 | function mod(c) { 4 | content = c; 5 | } 6 | 7 | mod.get = function () { 8 | return content; 9 | }; 10 | 11 | return mod; 12 | })(); 13 | 14 | (function (){ 15 | 16 | var etpl = require( '../../src/main' ); 17 | var readText = require( './readTextSync' ); 18 | var mytpl = new etpl.Engine(); 19 | var text = readText( 'engine.text.html' ); 20 | 21 | etpl.addFilter( 'engine-upper', function ( source ) { 22 | return source.toUpperCase(); 23 | } ); 24 | 25 | etpl.addFilter( 'engine-upper2', function ( source, initial ) { 26 | if (initial) { 27 | return source.charAt(0).toUpperCase() + source.slice( 1 ); 28 | } 29 | 30 | return source.toUpperCase(); 31 | } ); 32 | 33 | mytpl.addFilter( 'engine-upper', function ( source ) { 34 | return source.toUpperCase(); 35 | } ); 36 | 37 | mytpl.addFilter( 'engine-upper2', function ( source, initial ) { 38 | if (initial) { 39 | return source.charAt(0).toUpperCase() + source.slice( 1 ); 40 | } 41 | 42 | return source.toUpperCase(); 43 | } ); 44 | 45 | mytpl.addFilter( 'log', function ( source ) { 46 | engineLog(source); 47 | 48 | return source; 49 | } ); 50 | 51 | mytpl.addCommand('dump', { 52 | init: function () { 53 | var match = this.value.match(/^\s*([a-z0-9_]+)\s*$/i); 54 | if (!match) { 55 | throw new Error('Invalid ' + this.type + ' syntax: ' + this.value); 56 | } 57 | 58 | this.name = match[1]; 59 | this.cloneProps = ['name']; 60 | }, 61 | 62 | open: function (context) { 63 | context.stack.top().addChild(this); 64 | }, 65 | 66 | getRendererBody: function () { 67 | var util = etpl.util; 68 | var options = this.engine.options; 69 | return util.stringFormat( 70 | '{0};', 71 | util.compileVariable(options.variableOpen + this.name + '| log' + options.variableClose, this.engine) 72 | ); 73 | } 74 | }); 75 | 76 | describe('Engine', function() { 77 | it('can new by manual, isolate from default engine', function() { 78 | etpl.compile(text['default-tpl']); 79 | mytpl.compile(text['myengine-tpl']); 80 | expect(etpl.getRenderer('engineTarget')()) 81 | .toEqual(text['expect-default']); 82 | expect(mytpl.getRenderer('engineTarget')()) 83 | .toEqual(text['expect-myengine']); 84 | }); 85 | 86 | it('compile target which exists should throw error', function() { 87 | try{ 88 | etpl.compile(text['repeat-tpl']); 89 | etpl.compile(text['repeat-tpl']); 90 | expect(false).toBeTruthy(); 91 | } 92 | catch (ex) { 93 | var msg = ex.message; 94 | if ( /^\[ETPL_TARGET_EXISTS\]/i.test(msg) ) { 95 | expect(true).toBeTruthy(); 96 | } 97 | else { 98 | expect(false).toBeTruthy(); 99 | } 100 | } 101 | }); 102 | 103 | it('"config" method can setup command open and close', function() { 104 | mytpl.config({ 105 | commandOpen: '<%', 106 | commandClose: '%>' 107 | }); 108 | mytpl.compile(text['custom-options']); 109 | expect(mytpl.getRenderer('engineCustomTarget')()) 110 | .toEqual(text['expect-custom-options']); 111 | expect(mytpl.getRenderer('engineTarget')()) 112 | .toEqual(text['expect-myengine']); 113 | mytpl.config({ 114 | commandOpen: '' 116 | }); 117 | }); 118 | 119 | it('"config" method can setup command syntax', function() { 120 | mytpl.config({ 121 | commandSyntax: /^\s*(\/)?([a-z]+)\s?([\s\S]*)$/, 122 | commandOpen: '<%', 123 | commandClose: '%>', 124 | variableOpen: '{{', 125 | variableClose: '}}' 126 | }); 127 | var render = mytpl.compile(text['custom-syntax']); 128 | expect(render()).toEqual(text['expect-custom-syntax']); 129 | mytpl.config({ 130 | commandSyntax: /^\s*(\/)?([a-z]+)\s*(?::([\s\S]*))?$/, 131 | commandOpen: '', 133 | variableOpen: '${', 134 | variableClose: '}' 135 | }); 136 | }); 137 | 138 | it('"config" method can setup variable open and close', function() { 139 | mytpl.config({ 140 | variableOpen: '{{', 141 | variableClose: '}}' 142 | }); 143 | mytpl.compile(text['custom-variable']); 144 | expect(mytpl.getRenderer('engineCustomVariable')()) 145 | .toEqual(text['expect-custom-variable']); 146 | mytpl.config({ 147 | variableOpen: '${', 148 | variableClose: '}' 149 | }); 150 | }); 151 | 152 | it('"config" method can setup variable and command at the same time', function() { 153 | mytpl.config({ 154 | commandOpen: '<%', 155 | commandClose: '%>', 156 | variableOpen: '{{', 157 | variableClose: '}}' 158 | }); 159 | mytpl.compile(text['custom-command-variable']); 160 | expect(mytpl.getRenderer('engineCustomCommandVariable')()) 161 | .toEqual(text['expect-custom-command-variable']); 162 | mytpl.config({ 163 | commandOpen: '', 165 | variableOpen: '${', 166 | variableClose: '}' 167 | }); 168 | }); 169 | 170 | it('"config" method can setup default filter', function() { 171 | var data = {name: 'etpl'}; 172 | 173 | mytpl.compile(text['custom-filter-tpl']); 174 | expect(mytpl.render('engineCustomFilterTarget',data)).toEqual(text['expect-custom-filter-html']); 175 | 176 | mytpl.config({ 177 | defaultFilter: '' 178 | }); 179 | mytpl.compile(text['custom-filter-tpl2']); 180 | expect(mytpl.render('engineCustomFilterTarget2',data)).toEqual(text['expect-custom-filter-raw']); 181 | 182 | mytpl.config({ 183 | defaultFilter: 'html' 184 | }); 185 | }); 186 | 187 | it('"config" method can setup naming conflict "ignore"', function() { 188 | mytpl.config({ 189 | namingConflict: 'ignore' 190 | }); 191 | 192 | mytpl.compile(text['repeat-tpl-ignore-first']); 193 | mytpl.compile(text['repeat-tpl-ignore-second']); 194 | expect(mytpl.render('engineRepeatIgnoreTarget')).toEqual('ignore'); 195 | 196 | mytpl.config({ 197 | namingConflict: 'error' 198 | }); 199 | }); 200 | 201 | it('"config" method can setup naming conflict "override"', function() { 202 | mytpl.config({ 203 | namingConflict: 'override' 204 | }); 205 | 206 | mytpl.compile(text['repeat-tpl-override-first']); 207 | mytpl.compile(text['repeat-tpl-override-second']); 208 | expect(mytpl.render('engineRepeatOverrideTarget')).toEqual('override'); 209 | 210 | mytpl.config({ 211 | namingConflict: 'error' 212 | }); 213 | }); 214 | 215 | it('"config" method can setup "strip" to clean break line and whiteletter before and after command', function() { 216 | mytpl.config({ strip: true }); 217 | 218 | var render = mytpl.compile(text['strip-tpl']); 219 | expect(render(eval(text['strip-data']))).toEqual(text['strip-expect']); 220 | expect(mytpl.getRenderer('engineStripTargetSimple')()) 221 | .toEqual(text['strip-simple-expect']); 222 | expect(mytpl.getRenderer('engineStripTargetIf')()) 223 | .toEqual(text['strip-if-expect']); 224 | 225 | mytpl.config({ strip: false }); 226 | }); 227 | 228 | it('"render" method returns the same value as renderer call', function() { 229 | var renderer = mytpl.compile(text['variable-tpl']); 230 | var data = { 231 | info: { 232 | name:'etpl', 233 | contributor: 'errorrik' 234 | } 235 | }; 236 | expect(renderer(data)).toEqual(mytpl.render('engineVariableTarget',data)); 237 | }); 238 | 239 | it('"render" method can receive both plain object and object by getter method', function() { 240 | var renderer = etpl.compile(text['data-tpl']); 241 | var data = eval(text['data']); 242 | var dataGetter = eval(text['data-getter']) 243 | expect(renderer(data)).toEqual(renderer(dataGetter)); 244 | }); 245 | 246 | it('"addFilter" method can add a filter function', function() { 247 | var renderer = etpl.compile(text['upper-tpl']); 248 | var data = {name: 'etpl'}; 249 | expect(renderer(data)) 250 | .toEqual(text['expect-engineUpperTarget']); 251 | }); 252 | 253 | it('"addFilter" method can add a filter function which can receive extra params', function() { 254 | var renderer = etpl.compile(text['upper-tpl2']); 255 | var data = {name: 'etpl'}; 256 | expect(renderer(data)) 257 | .toEqual(text['expect-engineUpperTarget2']); 258 | }); 259 | 260 | it('"addCommand" method can custom command by yourself', function() { 261 | var renderer = mytpl.compile(text['tpl-dump-command']); 262 | var data = { 263 | person: { 264 | name:'erik', 265 | email:'errorrik@gmail.com' 266 | } 267 | }; 268 | expect(renderer(data)).toEqual(text['expect-dump-command']); 269 | expect(engineLog.get()).toBe(data.person); 270 | }); 271 | 272 | it('default instance: "parse" method should reserved for backward compatibility, same as "compile" method', function() { 273 | expect(etpl.parse).toBe(etpl.compile); 274 | }); 275 | }); 276 | 277 | })(); 278 | -------------------------------------------------------------------------------- /test/spec/engine.text.html: -------------------------------------------------------------------------------- 1 | ::default-tpl:: 2 | default engine 3 | ::myengine-tpl:: 4 | my engine 5 | ::variable-tpl:: 6 | Hello ${info.name}! 7 | ::repeat-tpl:: 8 | default engine 9 | ::repeat-tpl-ignore-first:: 10 | ignore 11 | ::repeat-tpl-ignore-second:: 12 | override 13 | ::repeat-tpl-override-first:: 14 | ignore 15 | ::repeat-tpl-override-second:: 16 | override 17 | ::data-tpl:: 18 | Hello ${info.name} by ${info.contributor.name}[${info.contributor.email}]! 19 | ::strip-tpl:: 20 | 21 |
      22 | 23 | 24 |
    • ${person}
    • 25 | 26 | 27 |
    28 | 29 | 30 | lang 31 | 32 | 33 | Hello World! 34 | ::strip-data:: 35 | ({persons:['erik', 'firede', 'leeight']}) 36 | ::strip-expect:: 37 |
      38 |
    • firede
    • 39 |
    • leeight
    • 40 |
    41 | 42 | ::strip-simple-expect:: 43 | Hello World! 44 | ::strip-if-expect:: 45 | lang 46 | 47 | ::custom-filter-tpl:: 48 | Hello ${name}! 49 | ::custom-filter-tpl2:: 50 | Hello ${name}! 51 | ::upper-tpl:: 52 | Hello ${name|engine-upper}! 53 | ::upper-tpl2:: 54 | Hello ${name|engine-upper2()}! Hello ${name | engine-upper2(true)}! 55 | ::data:: 56 | ({ 57 | info: { 58 | name: 'etpl', 59 | contributor: { 60 | name:'errorrik', 61 | email:'errorrik@gmail.com' 62 | } 63 | } 64 | }) 65 | ::data-getter:: 66 | ({ 67 | get: function (name){ 68 | if ( name == 'info.name' ) { 69 | return 'etpl'; 70 | } 71 | if ( name == 'info.contributor.name' ) { 72 | return 'errorrik'; 73 | } 74 | if ( name == 'info.contributor.email' ) { 75 | return 'errorrik@gmail.com'; 76 | } 77 | } 78 | }) 79 | ::custom-syntax:: 80 | <% var arr = new Array(5) %> 81 | <% for {{arr}} as {{item}}, {{index}} %> class="odd"<% /if %>>{{index}} 82 | <% /for %> 83 | ::expect-custom-syntax:: 84 | 85 |
  • 0
  • 86 |
  • 1
  • 87 |
  • 2
  • 88 |
  • 3
  • 89 |
  • 4
  • 90 | 91 | ::custom-options:: 92 | <% target: engineCustomTarget %> 93 | ::expect-default:: 94 | default engine 95 | ::expect-myengine:: 96 | my engine 97 | ::expect-custom-options:: 98 | 99 | ::custom-variable:: 100 | 101 | Hello {{name}}! Hello {{name | engine-upper}}! Hello {{name | engine-upper2( {{yes}} )}}! 102 | Hello {{name}}! 103 | ::expect-custom-variable:: 104 | 105 | Hello etpl! Hello ETPL! Hello Etpl! 106 | Hello Etpl! 107 | ::custom-command-variable:: 108 | <% target: engineCustomCommandVariable %><% var: name = "etpl" %><% var: yes = true %> 109 | Hello {{name}}! Hello {{name | engine-upper}}! Hello {{name | engine-upper2( {{yes}} )}}! 110 | <%if: {{yes}}%>Hello <%filter: engine-upper2({{yes}})%>{{name}}<%/filter%>!<%/if%> 111 | ::expect-custom-command-variable:: 112 | 113 | Hello etpl! Hello ETPL! Hello Etpl! 114 | Hello Etpl! 115 | ::expect-custom-filter-raw:: 116 | Hello etpl! 117 | ::expect-custom-filter-html:: 118 | Hello <b>etpl</b>! 119 | ::expect-engineUpperTarget:: 120 | Hello ETPL! 121 | ::expect-engineUpperTarget2:: 122 | Hello ETPL! Hello Etpl! 123 | ::tpl-dump-command:: 124 | ${person.name} - ${person.email} 125 | ::expect-dump-command:: 126 | erik - errorrik@gmail.com 127 | ::eof:: 128 | -------------------------------------------------------------------------------- /test/spec/filter.spec.js: -------------------------------------------------------------------------------- 1 | (function (){ 2 | 3 | var etpl = require( '../../src/main' ); 4 | var readText = require( './readTextSync' ); 5 | var text = readText( 'filter.text.html' ); 6 | 7 | etpl.addFilter( 'filter-lower', function (source, saveInitial) { 8 | if (saveInitial) { 9 | return source.charAt(0) + source.slice(1).toLowerCase(); 10 | } 11 | return source.toLowerCase(); 12 | }); 13 | 14 | etpl.addFilter( 'filter-not', function (source) { 15 | return !source; 16 | }); 17 | 18 | describe('Filter', function() { 19 | it('can filter a piece of text', function() { 20 | var renderer = etpl.compile( text['tpl-simple'] ); 21 | expect(renderer()).toEqual(text['expect-simple']); 22 | }); 23 | 24 | it('can be nested', function() { 25 | var renderer = etpl.compile( text['tpl-nested'] ); 26 | expect(renderer()).toEqual(text['expect-nested']); 27 | }); 28 | 29 | it('param can be passed', function() { 30 | var renderer = etpl.compile( text['tpl-param'] ); 31 | expect(renderer()).toEqual(text['expect-param']); 32 | }); 33 | 34 | it('param can use variable', function() { 35 | var renderer = etpl.compile( text['tpl-param-variable'] ); 36 | expect(renderer()).toEqual(text['expect-param']); 37 | }); 38 | 39 | it('param can use variable which has filter', function() { 40 | var renderer = etpl.compile( text['tpl-param-variable-filter'] ); 41 | expect(renderer()).toEqual(text['expect-param']); 42 | }); 43 | 44 | it('command literal allow break line', function() { 45 | var renderer = etpl.compile( text['tpl-param-break-line'] ); 46 | expect(renderer()).toEqual(text['expect-param']); 47 | }); 48 | 49 | it('has block in body, child target can override block content', function() { 50 | var renderer = etpl.compile( text['tpl-block'] ); 51 | expect(renderer({prod:'etpl'})).toEqual(text['expect-block-etpl']); 52 | expect(renderer({prod:'er'})).toEqual(text['expect-block-er']); 53 | }); 54 | }); 55 | })(); 56 | -------------------------------------------------------------------------------- /test/spec/filter.text.html: -------------------------------------------------------------------------------- 1 | ::tpl-simple:: 2 | 3 | Hello etpl! 4 | ::expect-simple:: 5 | 6 | <b>Hello etpl!</b> 7 | ::tpl-nested:: 8 | 9 | Hello etpl&er! 10 | ::expect-nested:: 11 | 12 | <b>Hello %3Cem%3Eetpl%26er%3C%2Fem%3E!</b> 13 | ::tpl-param:: 14 | 15 | ETPL ETPL 16 | ::tpl-param-variable:: 17 | 18 | ETPL ETPL 19 | ::tpl-param-variable-filter:: 20 | 21 | ETPL ETPL 22 | ::tpl-param-break-line:: 23 | 24 | ETPL ETPL 29 | ::expect-param:: 30 | 31 | etpl Etpl 32 | ::tpl-block:: 33 | 34 | ETPL 35 | ER 36 | 37 | 38 | ::expect-block-etpl:: 39 | 40 | etpl 41 | ::expect-block-er:: 42 | 43 | er 44 | ::eof:: 45 | -------------------------------------------------------------------------------- /test/spec/for.spec.js: -------------------------------------------------------------------------------- 1 | (function (){ 2 | 3 | var etpl = require( '../../src/main' ); 4 | var readText = require( './readTextSync' ); 5 | var text = readText( 'for.text.html' ); 6 | var data = { 7 | myList: eval(text.myList), 8 | myObj: eval(text.myObj), 9 | persons: eval(text.persons), 10 | personsIndex: eval(text.personsIndex) 11 | }; 12 | data.ecomfe = { 13 | persons: data.persons, 14 | personsIndex: data.personsIndex 15 | }; 16 | etpl.compile( text.tpl ); 17 | 18 | etpl.addFilter('for-incream', function (source, increament) { 19 | increament = increament || 1; 20 | var result = []; 21 | for (var i = 0; i < source.length; i++) { 22 | result[i] = source[i] + increament; 23 | } 24 | 25 | return result; 26 | }); 27 | 28 | describe('Array traversal', function() { 29 | it('can read "item" variable', function() { 30 | var renderer = etpl.getRenderer('forItemTarget'); 31 | expect(renderer(data)).toEqual(text['expect-forItemTarget']); 32 | }); 33 | 34 | it('can read "item" and "index" variables', function() { 35 | var renderer = etpl.getRenderer('forItemIndexTarget'); 36 | expect(renderer(data)).toEqual(text['expect-forItemIndexTarget']); 37 | }); 38 | 39 | it('can use property accessor in command tag', function() { 40 | var renderer = etpl.getRenderer('forItemCommandPropertyAccessTarget'); 41 | expect(renderer(data)).toEqual(text['expect-forItemCommandPropertyAccessTarget']); 42 | }); 43 | 44 | it('can use property accessor in traversal body', function() { 45 | var renderer = etpl.getRenderer('forItemPropertyAccessTarget'); 46 | expect(renderer(data)).toEqual(text['expect-forItemPropertyAccessTarget']); 47 | }); 48 | 49 | it('command literal allow break line', function() { 50 | var renderer = etpl.getRenderer('forItemIndexTargetBreakLine'); 51 | expect(renderer(data)).toEqual(text['expect-forItemIndexTarget']); 52 | }); 53 | 54 | it('can use filter to process "list" variable', function() { 55 | var renderer = etpl.getRenderer('forListFilterTarget'); 56 | expect(renderer(data)).toEqual(text['expect-forListFilterTarget']); 57 | }); 58 | 59 | it('can pass args when use filter to process "list" variable', function() { 60 | var renderer = etpl.getRenderer('forListFilterComplexTarget'); 61 | expect(renderer(data)).toEqual(text['expect-forListFilterComplexTarget']); 62 | }); 63 | }); 64 | 65 | describe('Object traversal', function() { 66 | it('can read "item" variable', function() { 67 | var renderer = etpl.getRenderer('forOItemTarget'); 68 | expect(renderer(data)).toEqual(text['expect-forOItemTarget']); 69 | }); 70 | 71 | it('can read "item" and "key" variables', function() { 72 | var renderer = etpl.getRenderer('forOItemKeyTarget'); 73 | expect(renderer(data)).toEqual(text['expect-forOItemKeyTarget']); 74 | }); 75 | 76 | it('can use property accessor in command tag', function() { 77 | var renderer = etpl.getRenderer('forOItemCommandPropertyAccessTarget'); 78 | expect(renderer(data)).toEqual(text['expect-forOItemCommandPropertyAccessTarget']); 79 | }); 80 | 81 | it('can use property accessor in traversal body', function() { 82 | var renderer = etpl.getRenderer('forOItemPropertyAccessTarget'); 83 | expect(renderer(data)).toEqual(text['expect-forOItemPropertyAccessTarget']); 84 | }); 85 | }); 86 | 87 | describe('Traversal', function() { 88 | it('can be nested', function() { 89 | var renderer = etpl.getRenderer('forNestedTarget'); 90 | var result = renderer(data); 91 | expect(result.indexOf(text['except-nested-1'])).toBeGreaterThan(-1); 92 | expect(result.indexOf(text['except-nested-2'])).toBeGreaterThan(-1); 93 | expect(result.indexOf(text['except-nested-3'])).toBeGreaterThan(-1); 94 | expect(result.indexOf(text['except-nested-4'])).toBeGreaterThan(-1); 95 | }); 96 | 97 | it('variable can be read when data has getter method', function() { 98 | var renderer = etpl.getRenderer('forDataGetterTarget'); 99 | var dataHasGetter = { 100 | get: function (name) { 101 | return data[ name ]; 102 | } 103 | }; 104 | var result = renderer(dataHasGetter); 105 | expect(result).toEqual(text['except-forDataGetterTarget']); 106 | }); 107 | 108 | it('has block in traversal body, child target can override block content', function() { 109 | var renderer = etpl.getRenderer('forBlockTarget'); 110 | expect(renderer(data)).toEqual(text['expect-forBlockTarget']); 111 | }); 112 | }); 113 | 114 | })(); 115 | -------------------------------------------------------------------------------- /test/spec/for.text.html: -------------------------------------------------------------------------------- 1 | ::tpl:: 2 | 3 | ${item}| 4 | 5 | ${item}| 6 | 7 | ${item}| 8 | 9 |
      10 |
    • ${person.name}[${person.email}]
    • 11 |
    12 | 13 |
      14 |
    • ${person.name}[${person.email}]
    • 15 |
    16 | 17 | |${item},${idx} 18 | 19 | |${item},${idx} 25 | 26 | ${item}| 27 | 28 | |${item},${key} 29 | 30 |
      31 |
    • ${person.name}[${person.email}]
    • 32 |
    33 | 34 |
      35 |
    • ${person.name}[${person.email}]
    • 36 |
    37 | 38 |
      39 |
    • ${person.name}[${person.email}]
    • 40 |
    41 | 42 |
  • ${person.name}[${person.email}]
  • 43 | 44 |
      45 | 46 |
    47 | 48 | 49 | 50 | ${key}:${value} 51 | 52 | 53 | ::myList:: 54 | [1,2,3,4,5] 55 | ::myObj:: 56 | ({ "0":1, "1":2, "2":3, "3":4, "4":5 }) 57 | ::persons:: 58 | [ 59 | {name: 'erik', email: 'errorrik@gmail.com'}, 60 | {name: 'firede', email: 'firede@gmail.com'} 61 | ] 62 | ::personsIndex:: 63 | ({ 64 | erik: {name: 'erik', email: 'errorrik@gmail.com'}, 65 | firede: {name: 'firede', email: 'firede@gmail.com'} 66 | }) 67 | ::expect-forItemTarget:: 68 | 69 | 1|2|3|4|5| 70 | 71 | ::expect-forListFilterTarget:: 72 | 73 | 2|3|4|5|6| 74 | 75 | ::expect-forListFilterComplexTarget:: 76 | 77 | 6|7|8|9|10| 78 | 79 | ::expect-forOItemTarget:: 80 | 81 | 1|2|3|4|5| 82 | 83 | ::expect-forItemPropertyAccessTarget:: 84 | 85 |
      86 |
    • erik[errorrik@gmail.com]
    • firede[firede@gmail.com]
    • 87 |
    88 | 89 | ::expect-forItemCommandPropertyAccessTarget:: 90 | 91 |
      92 |
    • erik[errorrik@gmail.com]
    • firede[firede@gmail.com]
    • 93 |
    94 | 95 | ::expect-forOItemPropertyAccessTarget:: 96 | 97 |
      98 |
    • erik[errorrik@gmail.com]
    • firede[firede@gmail.com]
    • 99 |
    100 | 101 | ::expect-forOItemCommandPropertyAccessTarget:: 102 | 103 |
      104 |
    • erik[errorrik@gmail.com]
    • firede[firede@gmail.com]
    • 105 |
    106 | 107 | ::expect-forItemIndexTarget:: 108 | 109 | 1,0|2,1|3,2|4,3|5,4 110 | 111 | ::expect-forOItemKeyTarget:: 112 | 113 | 1,0|2,1|3,2|4,3|5,4 114 | 115 | ::except-nested-1:: 116 | name:erik 117 | ::except-nested-2:: 118 | email:errorrik@gmail.com 119 | ::except-nested-3:: 120 | name:firede 121 | ::except-nested-4:: 122 | email:firede@gmail.com 123 | ::except-forDataGetterTarget:: 124 | 125 |
      126 |
    • erik[errorrik@gmail.com]
    • firede[firede@gmail.com]
    • 127 |
    128 | 129 | ::expect-forBlockTarget:: 130 | 131 |
      132 |
    • erik[errorrik@gmail.com]
    • firede[firede@gmail.com]
    • 133 |
    134 | 135 | ::eof:: 136 | -------------------------------------------------------------------------------- /test/spec/if.spec.js: -------------------------------------------------------------------------------- 1 | (function (){ 2 | 3 | var etpl = require( '../../src/main' ); 4 | var readText = require( './readTextSync' ); 5 | var text = readText( 'if.text.html' ); 6 | etpl.compile( text.tpl ); 7 | 8 | etpl.addFilter('if-sum', function (source) { 9 | for (var i = 1; i < arguments.length; i++) { 10 | source += arguments[i]; 11 | } 12 | 13 | return source; 14 | }); 15 | 16 | describe('Conditional', function() { 17 | it('use "if-elif-else" should be render correctly', function() { 18 | var renderer = etpl.getRenderer('ifNumTarget'); 19 | expect(renderer({num:1})).toEqual(text['expect-number-1']); 20 | expect(renderer({num:0})).toEqual(text['expect-number-0']); 21 | expect(renderer({num:-1})).toEqual(text['expect-number-invalid']); 22 | }); 23 | 24 | it('use unary expr in "if" should be render correctly', function() { 25 | var renderer = etpl.getRenderer('ifUnaryTarget'); 26 | expect(renderer( {num:1,str:"1"} )).toEqual(text['expect-ifUnaryTarget']); 27 | }); 28 | 29 | it('use binary expr in "if" should be render correctly', function() { 30 | var renderer = etpl.getRenderer('ifBinaryTarget'); 31 | expect(renderer( {num:1,str:"1"} )).toEqual(text['expect-ifBinaryTarget']); 32 | }); 33 | 34 | it('use complex expr in "if" should be render correctly', function() { 35 | var renderer = etpl.getRenderer('ifComplexTarget'); 36 | expect(renderer( {num:1,str:"1"} )).toEqual(text['expect-ifComplexTarget']); 37 | }); 38 | 39 | it('can use complex property accessor in variable', function() { 40 | var renderer = etpl.getRenderer('ifComplexPropertyAccessTarget'); 41 | expect(renderer( {level1: [ 1, {num:1} ] } )).toEqual(text['expect-ifComplexTarget']); 42 | }); 43 | 44 | it('can use filter in variable when command if', function() { 45 | var renderer = etpl.compile(text['tpl-variable-filter-if']); 46 | expect(renderer()).toEqual(text['expect-variable-filter-if']); 47 | }); 48 | 49 | it('can use filter in variable when command elif', function() { 50 | var renderer = etpl.compile(text['tpl-variable-filter-elif']); 51 | expect(renderer()).toEqual(text['expect-variable-filter-elif']); 52 | }); 53 | 54 | it('command literal allow break line', function() { 55 | var renderer = etpl.getRenderer('ifComplexTargetBreakLine'); 56 | expect(renderer( {num:1,str:"1"} )).toEqual(text['expect-ifComplexTarget']); 57 | }); 58 | 59 | it('has block in body, child target can override block content', function() { 60 | var renderer = etpl.getRenderer('ifBlockTarget'); 61 | expect(renderer({cond: 'if'})).toEqual(text['expect-ifBlockTarget-if']); 62 | expect(renderer({cond: 'elif'})).toEqual(text['expect-ifBlockTarget-elif']); 63 | expect(renderer({cond: 'else'})).toEqual(text['expect-ifBlockTarget-else']); 64 | }); 65 | 66 | it('"if" can be nested', function() { 67 | var renderer = etpl.getRenderer('ifNestedTarget'); 68 | expect(renderer( {num:1,str:"1"} )) 69 | .toEqual(text['expect-ifNestedTarget-samevalue']); 70 | expect(renderer( {num:2,str:"1"} )) 71 | .toEqual(text['expect-ifNestedTarget-differentvalue']); 72 | }); 73 | }); 74 | 75 | })(); 76 | -------------------------------------------------------------------------------- /test/spec/if.text.html: -------------------------------------------------------------------------------- 1 | ::tpl:: 2 | 3 | ${num}zeroinvalid 6 | 7 | 1 8 | 1 9 | 12 10 | 11 | 1 12 | 1 13 | 1 14 | 12 15 | ${str}2 16 | ${str}2 17 | 18 | 12 19 | 12 20 | 21 | 12 26 | 12 30 | 31 | 12 32 | 12 33 | 34 | 35 | 36 | if 37 | elif 38 | else 39 | 40 | 41 | same typedifferent type 42 | 43 | different value 44 | 45 | 46 | ::expect-ifUnaryTarget:: 47 | 48 | 1 49 | 1 50 | 2 51 | 52 | ::expect-ifBinaryTarget:: 53 | 54 | 1 55 | 1 56 | 1 57 | 2 58 | 1 59 | 2 60 | 61 | ::expect-ifComplexTarget:: 62 | 63 | 1 64 | 2 65 | 66 | ::expect-ifComplexTarget:: 67 | 68 | 1 69 | 2 70 | 71 | ::expect-ifNestedTarget-samevalue:: 72 | 73 | 74 | different type 75 | 76 | 77 | ::expect-ifNestedTarget-differentvalue:: 78 | 79 | 80 | different value 81 | 82 | 83 | ::expect-number-1:: 84 | 85 | 1 86 | 87 | ::expect-number-0:: 88 | 89 | zero 90 | 91 | ::expect-number-invalid:: 92 | 93 | invalid 94 | 95 | ::tpl-variable-filter-if:: 96 | 97 | oknot ok 98 | ::expect-variable-filter-if:: 99 | 100 | ok 101 | ::tpl-variable-filter-elif:: 102 | 103 | not okoknot ok 104 | ::expect-variable-filter-elif:: 105 | 106 | ok 107 | ::expect-ifBlockTarget-if:: 108 | 109 | if 110 | 111 | ::expect-ifBlockTarget-elif:: 112 | 113 | elif 114 | 115 | ::expect-ifBlockTarget-else:: 116 | 117 | else 118 | 119 | ::eof:: 120 | -------------------------------------------------------------------------------- /test/spec/import.spec.js: -------------------------------------------------------------------------------- 1 | (function (){ 2 | 3 | var etpl = require( '../../src/main' ); 4 | var readText = require( './readTextSync' ); 5 | var text = readText( 'import.text.html' ); 6 | var data = {name: text.name}; 7 | 8 | describe('Import', function() { 9 | it('can be used in target', function() { 10 | var renderer = etpl.compile(text['tpl-para']); 11 | expect(renderer(data)).toEqual(text['expect-para']); 12 | }); 13 | 14 | it('can be used in target which close manually', function() { 15 | var renderer = etpl.compile(text['tpl-hasclose']); 16 | expect(renderer(data)).toEqual(text['expect-hasclose']); 17 | }); 18 | 19 | it('can be used in if command', function() { 20 | var renderer = etpl.compile(text['tpl-inif']); 21 | expect(renderer(data)).toEqual(text['expect-inif']); 22 | }); 23 | 24 | it('can be used in for command', function() { 25 | var renderer = etpl.compile(text['tpl-infor']); 26 | expect(renderer(data)).toEqual(text['expect-infor']); 27 | }); 28 | 29 | it('can be used in master applyed', function() { 30 | var renderer = etpl.compile(text['tpl-master']); 31 | expect(renderer(data)).toEqual(text['expect-master']); 32 | }); 33 | 34 | it('noexist target, engine should throw an error when missTarget config is "error"', function() { 35 | var mytpl = new etpl.Engine({missTarget: 'error'}); 36 | try{ 37 | var renderer = mytpl.compile(text['tpl-miss']); 38 | expect(false).toBeTruthy(); 39 | } 40 | catch (ex) { 41 | var msg = ex.message; 42 | if (/^\[ETPL_MISS_TARGET\]importMissInFor/i.test(msg) 43 | && msg.indexOf('importMissInFor') > 0 44 | ) { 45 | expect(true).toBeTruthy(); 46 | } 47 | else { 48 | expect(false).toBeTruthy(); 49 | } 50 | } 51 | }); 52 | 53 | it('noexist target, engine should not throw an error when missTarget config is default value', function() { 54 | var mytpl = new etpl.Engine(); 55 | try{ 56 | var renderer = mytpl.compile(text['tpl-miss']); 57 | expect(renderer).toBeNull(); 58 | expect(mytpl.render('importMiss')).toEqual(''); 59 | } 60 | catch (ex) { 61 | expect(false).toBeTruthy(); 62 | } 63 | }); 64 | 65 | it('When dependence unready, getRenderer should return null. Renderer can be getted when dependence ready.', function() { 66 | var renderer = etpl.compile(text['tpl-unready-begin']); 67 | expect(renderer).toBeNull(); 68 | 69 | etpl.compile(text['tpl-unready-then']); 70 | renderer = etpl.getRenderer('importUnready'); 71 | expect(typeof renderer).toBe('function'); 72 | expect(renderer(data)).toBe(text['expect-unready']); 73 | }); 74 | 75 | it('can override block which declared in target', function() { 76 | var renderer = etpl.compile(text['tpl-block']); 77 | expect(renderer()).toEqual(text['expect-block']); 78 | }); 79 | 80 | it('can be nested', function() { 81 | var renderer = etpl.compile(text['tpl-nested']); 82 | expect(renderer()).toEqual(text['expect-nested']); 83 | }); 84 | 85 | it('noexist target in nested struction, engine should throw an correct error', function() { 86 | var mytpl = new etpl.Engine({missTarget: 'error'}); 87 | try{ 88 | var renderer = mytpl.compile(text['tpl-miss-nested']); 89 | expect(false).toBeTruthy(); 90 | } 91 | catch (ex) { 92 | var msg = ex.message; 93 | if (/^\[ETPL_MISS_TARGET\]importMissNestedAbstractCrumb/i.test(msg) 94 | && msg.indexOf('importMissNestedCrumb') > 0 95 | ) { 96 | expect(true).toBeTruthy(); 97 | } 98 | else { 99 | expect(false).toBeTruthy(); 100 | } 101 | } 102 | }); 103 | 104 | it('and override block, the own target can be extended', function() { 105 | var renderer = etpl.compile(text['tpl-mix']); 106 | expect(renderer()).toEqual(text['expect-mix-target']); 107 | 108 | renderer = etpl.getRenderer('importMixMaster'); 109 | expect(renderer()).toEqual(text['expect-mix-master']); 110 | }); 111 | }); 112 | 113 | })(); 114 | -------------------------------------------------------------------------------- /test/spec/import.text.html: -------------------------------------------------------------------------------- 1 | ::name:: 2 | etpl 3 | ::tpl-para:: 4 |

    5 | Hello, I am ${name}! 6 | ::expect-para:: 7 |

    Hello, I am etpl!

    8 | 9 | ::tpl-hasclose:: 10 | 11 | 12 |
    Hello ${name}!
    13 | 14 | 15 | 16 |
    Header Content
    17 | 18 |
    Footer Content
    19 | ::expect-hasclose:: 20 | 21 |
    Header Content
    22 |
    Hello etpl!
    23 |
    Footer Content
    24 | 25 | ::tpl-inif:: 26 | 27 | In if. 28 | Hello ${name}! 29 | ::expect-inif:: 30 | 31 | Hello etpl! In if. 32 | 33 | ::tpl-infor:: 34 | 35 | in for| 36 | ${index} 37 | ::expect-infor:: 38 | 39 | 0 in for|1 in for|2 in for| 40 | 41 | ::tpl-miss:: 42 | 43 | 44 | in for| 45 | ::tpl-master:: 46 | 47 | 48 | 49 |
    50 |
    51 | Hello, I am ${name}!Copyright ${name} 52 | ::expect-master:: 53 | 54 |
    Hello, I am etpl!
    55 |
    Copyright etpl
    56 | 57 | ::tpl-unready-begin:: 58 | 59 | 60 | 61 |
    62 |
    63 | 64 | ::tpl-unready-then:: 65 | Hello, ${name}!Copyright ${name} 2013 66 | ::expect-unready:: 67 | 68 |
    Hello, etpl!
    69 |
    Copyright etpl 2013
    70 | 71 | ::tpl-block:: 72 | 73 | 74 | 75 | biz list 76 | biz pager 77 | 78 | 79 |
    default header
    80 |
    default main
    81 | 82 |
    default footer
    83 |
    default filter
    84 |
    default list
    85 |
    default pager
    86 | ::expect-block:: 87 | 88 |
    default header
    89 | 90 |
    91 |
    biz list
    92 | 93 |
    biz pager
    94 |
    95 | 96 | 97 | 98 | ::tpl-mix:: 99 | 100 | target head 101 | target foot 102 | 103 |
    master head
    104 |
    master hello
    105 |
    106 | default hello 107 |

    CopyRight Etpl 108 | ::expect-mix-target:: 109 | 110 |
    target head
    111 |
    master hello 112 |
    113 |
    target foot
    114 | 115 | ::expect-mix-master:: 116 | 117 |
    master head
    118 |
    master hello 119 |
    120 |

    CopyRight Etpl

    121 | 122 | ::tpl-nested:: 123 | 124 | 125 | 126 | World 127 | 128 | 129 | 130 | 131 | example 132 | 133 | 134 | 135 |
    136 | 137 | 138 |
    139 |
    140 | 141 | 142 |
    143 | ::expect-nested:: 144 | 145 | 146 |
    147 | 148 | 149 |
    example
    150 | 151 | 152 | 153 |
    154 |
    World
    155 | 156 | 157 | 158 | 159 | ::tpl-miss-nested:: 160 | 161 | 162 | 163 | World 164 | 165 | 166 | 167 | 168 | example 169 | 170 | 171 | 172 |
    173 | 174 | 175 |
    176 |
    177 | ::eof:: 178 | -------------------------------------------------------------------------------- /test/spec/node.spec.js: -------------------------------------------------------------------------------- 1 | (function (){ 2 | 3 | var etpl = require( '../../src/node' ); 4 | etpl.config({ 5 | dir: __dirname 6 | }); 7 | 8 | var data = {name: 'errorrik'}; 9 | var readText = require( './readTextSync' ); 10 | var text = readText( 'node.spec.text' ); 11 | 12 | describe('Node env', function() { 13 | it('can load the import target automatically', function() { 14 | var renderer = etpl.load('node/main'); 15 | expect(renderer(data)).toEqual(text['expect-main']); 16 | }); 17 | 18 | it('can load the master target automatically', function() { 19 | var renderer = etpl.load('node/master-main'); 20 | expect(renderer(data)).toEqual(text['expect-master-main']); 21 | }); 22 | 23 | }); 24 | 25 | })(); 26 | -------------------------------------------------------------------------------- /test/spec/node.spec.text: -------------------------------------------------------------------------------- 1 | ::expect-main:: 2 | 3 |
    Header Content
    4 |
    Hello errorrik!
    5 |
    Footer Content
    6 | 7 | ::expect-master-main:: 8 | 9 |
    Header Content
    10 | 11 |
    Hello errorrik!
    12 |
    Footer Content
    13 | 14 | 15 | ::eof:: 16 | -------------------------------------------------------------------------------- /test/spec/node/footer.etpl: -------------------------------------------------------------------------------- 1 |
    Footer Content
    2 | -------------------------------------------------------------------------------- /test/spec/node/header.etpl: -------------------------------------------------------------------------------- 1 |
    Header Content
    2 | -------------------------------------------------------------------------------- /test/spec/node/main.etpl: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    Hello ${name}!
    4 | 5 | -------------------------------------------------------------------------------- /test/spec/node/master-main.etpl: -------------------------------------------------------------------------------- 1 | 2 | Hello ${name}! 3 | Footer Content 4 | -------------------------------------------------------------------------------- /test/spec/node/master-root.etpl: -------------------------------------------------------------------------------- 1 | 2 |
    header
    3 | content 4 | -------------------------------------------------------------------------------- /test/spec/node/master-sub.etpl: -------------------------------------------------------------------------------- 1 | 2 | Header Content 3 | 4 |
    5 |
    6 | 7 | -------------------------------------------------------------------------------- /test/spec/readTextSync.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var isBrowser = typeof window != 'undefined' && typeof navigator != 'undefined'; 3 | 4 | function readTextSync( path ) { 5 | function parseText( content ) { 6 | var lines = content.replace(/\r?\n/g, '\n').split( '\n' ); 7 | var result = {}; 8 | 9 | var itemName; 10 | var buf = []; 11 | function flushItem() { 12 | if ( itemName ) { 13 | result[ itemName ] = buf.join( '\n' ); 14 | } 15 | } 16 | 17 | for ( var i = 0; i < lines.length; i++ ) { 18 | var line = lines[ i ]; 19 | 20 | if ( /^\s*::\s*([a-z0-9_-]+)\s*::\s*$/i.test( line ) ) { 21 | flushItem(); 22 | 23 | itemName = RegExp.$1; 24 | buf = []; 25 | } 26 | else { 27 | buf.push( line ); 28 | } 29 | } 30 | 31 | flushItem(); 32 | return result; 33 | } 34 | 35 | if ( isBrowser ) { 36 | var xhr = window.XMLHttpRequest 37 | ? new XMLHttpRequest() 38 | : new ActiveXObject( 'Microsoft.XMLHTTP' ); 39 | xhr.open( 'GET', 'spec/' + path, false ); 40 | xhr.send( null ); 41 | 42 | if ( xhr.status >= 200 && xhr.status < 300 ) { 43 | return parseText( xhr.responseText ); 44 | } 45 | } 46 | else { 47 | var fs = require( 'fs' ); 48 | return parseText( 49 | fs.readFileSync( 50 | require( 'path' ).resolve( __dirname, path ), 51 | 'UTF8' 52 | ) 53 | ); 54 | } 55 | 56 | return null; 57 | } 58 | 59 | if ( isBrowser ) { 60 | window.readTextSync = readTextSync; 61 | } 62 | else { 63 | module.exports = exports = readTextSync; 64 | } 65 | })(); 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /test/spec/target.spec.js: -------------------------------------------------------------------------------- 1 | (function (){ 2 | 3 | var etpl = require( '../../src/main' ); 4 | var readText = require( './readTextSync' ); 5 | var text = readText( 'target.text.html' ); 6 | 7 | describe('Target', function() { 8 | it( 9 | 'not exist should return undefined when be getted', 10 | function() { 11 | expect(etpl.getRenderer('noExist')).toBeUndefined(); 12 | } 13 | ); 14 | 15 | it( 16 | 'should parse correctly when closed manually', 17 | function() { 18 | var render = etpl.compile(text['tpl-closed-manually']); 19 | expect(render()).toEqual(text['expect-closed-manually']); 20 | expect(render).toBe(etpl.getRenderer('targetClosedManually')); 21 | } 22 | ); 23 | 24 | it( 25 | 'should parse correctly when auto closed', 26 | function() { 27 | var render = etpl.compile(text['tpl-autoclose']); 28 | expect(render()).toEqual(text['expect-autoclose']); 29 | expect(render).toBe(etpl.getRenderer('targetAutoClose')); 30 | } 31 | ); 32 | 33 | it( 34 | 'should parse correctly when shorthand', 35 | function() { 36 | var render = etpl.compile(text['tpl-shorthand']); 37 | expect(render()).toEqual(text['expect-shorthand']); 38 | expect(render).toBe(etpl.getRenderer('targetShorthand')); 39 | } 40 | ); 41 | 42 | it( 43 | 'parse empty string should return a renderer which return empty string ""', 44 | function() { 45 | expect(etpl.compile('')()).toEqual(''); 46 | } 47 | ); 48 | 49 | it( 50 | 'compile method should return renderer of first target, and then all renderers can be getted', 51 | function() { 52 | var render = etpl.compile( text['tpl-many'] ); 53 | 54 | expect(render).toBe(etpl.getRenderer('targetMany1')); 55 | expect(typeof etpl.getRenderer('targetMany1')).toBe('function'); 56 | expect(typeof etpl.getRenderer('targetMany2')).toBe('function'); 57 | expect(typeof etpl.getRenderer('targetMany3')).toBe('function'); 58 | expect(etpl.getRenderer('targetMany1')()).toEqual(text['expect-many1']); 59 | expect(etpl.getRenderer('targetMany2')()).toEqual(text['expect-many2']); 60 | expect(etpl.getRenderer('targetMany3')()).toEqual(text['expect-many3']); 61 | } 62 | ); 63 | 64 | it( 65 | 'can be extends from master target', 66 | function() { 67 | var render = etpl.compile(text['tpl-simple-master']); 68 | expect(render()).toEqual(text['expect-simple-master']); 69 | 70 | render = etpl.compile(text['tpl-simple-master2']); 71 | expect(render()).toEqual(text['expect-simple-master2']); 72 | 73 | render = etpl.getRenderer('targetFromMasterSimple2Breakline'); 74 | expect(render()).toEqual(text['expect-simple-master2']); 75 | } 76 | ); 77 | 78 | it( 79 | 'use master block content when not have block in self', 80 | function() { 81 | etpl.compile(text['tpl-master-default-block']); 82 | var render = etpl.getRenderer('targetFromDefaultBlock/Master'); 83 | expect(render()).toEqual(text['expect-master-default-block']); 84 | } 85 | ); 86 | 87 | it( 88 | 'use noexists master should throw an error when missTarget config is "error"', 89 | function() { 90 | var mytpl = new etpl.Engine({missTarget: 'error'}); 91 | try{ 92 | var renderer = mytpl.compile(text['tpl-miss-master']); 93 | expect(false).toBeTruthy(); 94 | } 95 | catch (ex) { 96 | var msg = ex.message; 97 | if (/^\[ETPL_MISS_TARGET\]targetMissMaster\/Master\/Master/i.test(msg) 98 | && msg.indexOf('targetMissMaster/MasterT') > 0 99 | ) { 100 | expect(true).toBeTruthy(); 101 | } 102 | else { 103 | expect(false).toBeTruthy(); 104 | } 105 | } 106 | } 107 | ); 108 | 109 | it( 110 | 'use noexists master should throw an error when missTarget config is "error", after recompile relate target, it should be render correctly', 111 | function() { 112 | var mytpl = new etpl.Engine({missTarget: 'error'}); 113 | try{ 114 | var renderer = mytpl.compile(text['tpl-lazy-target']); 115 | expect(false).toBeTruthy(); 116 | } 117 | catch (ex) { 118 | var msg = ex.message; 119 | if (/^\[ETPL_MISS_TARGET\]targetLazy\/Master/i.test(msg) 120 | && msg.indexOf('targetFromLazyMaster') > 0 121 | ) { 122 | expect(true).toBeTruthy(); 123 | mytpl.compile(text['tpl-lazy-master']); 124 | var renderer = mytpl.getRenderer('targetFromLazyMaster'); 125 | expect(typeof renderer).toBe('function'); 126 | expect(renderer()).toBe(text['expect-lazy']); 127 | } 128 | else { 129 | expect(false).toBeTruthy(); 130 | } 131 | } 132 | } 133 | ); 134 | 135 | it( 136 | 'can be extends from target which extends from other parent target', 137 | function() { 138 | etpl.compile( text['tpl-ntier-master'] ); 139 | expect(etpl.getRenderer('targetNTierNoContent')()) 140 | .toEqual(text[ 'expect-ntier-nocontent' ]); 141 | expect(etpl.getRenderer('targetNTierBodyContent')()) 142 | .toEqual(text[ 'expect-ntier-bodycontent' ]); 143 | expect(etpl.getRenderer('targetNTierBHContent')()) 144 | .toEqual(text[ 'expect-ntier-bhcontent' ]); 145 | } 146 | ); 147 | 148 | it( 149 | 'extends uncompiled master will return null. After master compiled, renderer canbe getted.', 150 | function() { 151 | var render = etpl.compile( text['tpl-lazy-target'] ); 152 | expect(render).toBeNull(); 153 | 154 | etpl.compile( text['tpl-lazy-master'] ); 155 | render = etpl.getRenderer('targetFromLazyMaster'); 156 | expect(typeof render).toBe('function'); 157 | expect(render()).toBe(text['expect-lazy']); 158 | } 159 | ); 160 | 161 | it( 162 | 'block can be nested', 163 | function() { 164 | var render = etpl.compile(text['tpl-nested-block']); 165 | expect(render()).toEqual(text['expect-nested-block']); 166 | 167 | render = etpl.getRenderer('targetFromNestedBlockNoHeader'); 168 | expect(render()).toEqual(text['expect-nested-block-noheader']); 169 | 170 | render = etpl.getRenderer('targetFromNestedBlockCustomBody'); 171 | expect(render()).toEqual(text['expect-nested-block-custombody']); 172 | } 173 | ); 174 | }); 175 | 176 | })(); 177 | -------------------------------------------------------------------------------- /test/spec/target.text.html: -------------------------------------------------------------------------------- 1 | ::tpl-closed-manually:: 2 | 3 | abcdefg 4 | 5 | ::expect-closed-manually:: 6 | 7 | abcdefg 8 | 9 | ::tpl-autoclose:: 10 | 11 | abcdefg 12 | 13 | ::expect-autoclose:: 14 | 15 | abcdefg 16 | 17 | ::tpl-shorthand:: 18 | 19 | shorthand 20 | 21 | ::expect-shorthand:: 22 | 23 | shorthand 24 | 25 | ::tpl-many:: 26 | 27 | manyTargets-1 28 | 29 | manyTargets-2 30 | 31 | manyTargets-3 32 | 33 | ::expect-many1:: 34 | 35 | manyTargets-1 36 | 37 | ::expect-many2:: 38 | 39 | manyTargets-2 40 | 41 | ::expect-many3:: 42 | 43 | manyTargets-3 44 | 45 | ::tpl-simple-master:: 46 | 47 | abc 48 | 49 | def 50 | 51 | ::expect-simple-master:: 52 | abcdef 53 | 54 | ::tpl-simple-master2:: 55 | 56 | header 57 | body 58 | footer 59 | 64 | header 65 | body 66 | footer 67 | 68 |
    69 |
    70 |
    71 | ::expect-simple-master2:: 72 | 73 |
    header
    74 |
    body
    75 |
    footer
    76 | ::tpl-master-default-block:: 77 | 78 |
    header
    79 |
    body
    80 | 81 | body2 82 | 83 | ::expect-master-default-block:: 84 | 85 |
    header
    86 |
    body2
    87 | 88 | ::tpl-ntier-master:: 89 | 90 |
    header
    91 |
    body
    92 | 99 | child header 100 | 101 | child head 102 | child body 103 | 104 | 105 | 106 | 107 | child body2 108 | 109 | child head2 110 | ::expect-ntier-nocontent:: 111 | 112 |
    child header
    113 |
    114 | child head 115 | child body 116 |
    117 | 118 | ::expect-ntier-bodycontent:: 119 | 120 |
    child header
    121 |
    122 | child head 123 | child body2 124 |
    125 | 126 | ::expect-ntier-bhcontent:: 127 | 128 |
    child header
    129 |
    130 | child head2 131 | child body 132 |
    133 | 134 | ::tpl-lazy-target:: 135 | 136 | content 137 | ::tpl-lazy-master:: 138 | 139 | ::expect-lazy:: 140 | content 141 | ::tpl-miss-master:: 142 | 143 | content 144 | heiheihei 145 | ::tpl-nested-block:: 146 | 147 | body main 148 | body side 149 | header 150 | 151 | 152 | body main 153 | body side 154 | 155 | 156 | 157 | 158 |
    body main
    159 | 160 | header 161 | 162 | 163 |
    164 |
    165 | 166 |
    167 |
    168 | ::expect-nested-block:: 169 | 170 |
    header
    171 |
    172 | 173 |
    body main
    174 |
    175 | ::expect-nested-block-noheader:: 176 | 177 | 178 |
    179 | 180 |
    body main
    181 |
    182 | ::expect-nested-block-custombody:: 183 | 184 |
    header
    185 |
    186 |
    body main
    187 |
    188 | ::eof:: 189 | -------------------------------------------------------------------------------- /test/spec/use.spec.js: -------------------------------------------------------------------------------- 1 | (function (){ 2 | 3 | var etpl = require( '../../src/main' ); 4 | var readText = require( './readTextSync' ); 5 | var text = readText( 'use.text.html' ); 6 | var data = { 7 | persons: eval(text['data-persons']), 8 | engine: text['data-engine'] 9 | }; 10 | etpl.compile( text.tpl ); 11 | 12 | etpl.addFilter('use-prefix', function (source, prefix) { 13 | prefix = prefix || 'sb'; 14 | return prefix + '-' + source; 15 | }); 16 | 17 | describe('Use', function() { 18 | it('can pass arguments to call a target', function() { 19 | var renderer = etpl.getRenderer('useSimpleTarget'); 20 | expect(renderer(data)).toEqual(text['expect-useSimpleTarget']); 21 | }); 22 | 23 | it('command literal allow break line', function() { 24 | var renderer = etpl.getRenderer('useSimpleTargetBreakLine'); 25 | expect(renderer(data)).toEqual(text['expect-useSimpleTarget']); 26 | }); 27 | 28 | it('can not read data of caller', function() { 29 | var renderer = etpl.getRenderer('useEngineTarget'); 30 | expect(renderer(data)).toEqual(text['expect-useEngineTarget']); 31 | }); 32 | 33 | it('When dependence unready, getRenderer should return null. Renderer can be getted when dependence ready.', function() { 34 | etpl.compile(text['tpl-dep-target']); 35 | var renderer = etpl.getRenderer('useDepTarget'); 36 | expect(renderer).toBeNull(); 37 | 38 | etpl.compile(text['tpl-dep-master']) 39 | renderer = etpl.getRenderer('useDepTarget'); 40 | expect(renderer(data)).toEqual(text['expect-useDepTarget']); 41 | }); 42 | 43 | it('yourself should be allowed (recursion)', function() { 44 | etpl.compile(text['tpl-recursion']) 45 | var renderer = etpl.getRenderer('useRecursionTarget'); 46 | expect(renderer(eval(text['data-recursion']))) 47 | .toEqual(text['expect-useRecursionTarget']); 48 | }); 49 | 50 | it('can use filter to process arguments', function() { 51 | var renderer = etpl.getRenderer('useSimpleFilterTarget'); 52 | expect(renderer(data)).toEqual(text['expect-useSimpleFilterTarget']); 53 | }); 54 | 55 | it('can pass args when use filter to process arguments', function() { 56 | var renderer = etpl.getRenderer('useSimpleFilterArgTarget'); 57 | expect(renderer(data)).toEqual(text['expect-useSimpleFilterArgTarget']); 58 | }); 59 | }); 60 | 61 | })(); -------------------------------------------------------------------------------- /test/spec/use.text.html: -------------------------------------------------------------------------------- 1 | ::tpl:: 2 | Engine(${engine}) 3 | Engine(${engine}) 4 | 5 |
      6 | 7 | 8 | 9 |
    10 | 11 |
      12 | 13 | 14 | 15 |
    16 | 17 |
      18 | 19 | 20 | 21 |
    22 | 23 |
      24 | 25 | 31 | 32 |
    33 |
  • ${name}[${email}] 34 | ::tpl-dep-target:: 35 | 36 | 37 | ::tpl-dep-master:: 38 | 39 |
    40 |
    41 | ${name} 42 | header 43 | ::tpl-recursion:: 44 | 45 | 46 | 47 | 54 | 55 | ::data-recursion:: 56 | ({ 57 | menu: [ 58 | {text: 'a'}, 59 | {text: 'b', children: [ 60 | {text: 'b1', children:[ 61 | {text: 'b11'}, 62 | {text: 'b12'} 63 | ]}, 64 | {text: 'b2'}, 65 | {text: 'b3'} 66 | ]}, 67 | {text: 'c'} 68 | ] 69 | }) 70 | ::expect-useRecursionTarget:: 71 | 72 |
      73 | 74 |
    • a 75 | 76 |
    • 77 | 78 |
    • b 79 | 80 |
        81 | 82 |
      • b1 83 | 84 |
          85 | 86 |
        • b11 87 | 88 |
        • 89 | 90 |
        • b12 91 | 92 |
        • 93 | 94 |
        95 | 96 |
      • 97 | 98 |
      • b2 99 | 100 |
      • 101 | 102 |
      • b3 103 | 104 |
      • 105 | 106 |
      107 | 108 |
    • 109 | 110 |
    • c 111 | 112 |
    • 113 | 114 |
    115 | 116 | ::data-persons:: 117 | [ 118 | {name:'errorrik', email:'errorrik@gmail.com'}, 119 | {name:'otakustay', email:'otakustay@gmail.com'} 120 | ] 121 | ::data-engine:: 122 | default 123 | ::expect-useSimpleTarget:: 124 | 125 |
      126 | 127 |
    • errorrik[errorrik@gmail.com] 128 | 129 |
    • otakustay[otakustay@gmail.com] 130 | 131 |
    132 | 133 | ::expect-useSimpleFilterTarget:: 134 | 135 |
      136 | 137 |
    • sb-errorrik[errorrik@gmail.com] 138 | 139 |
    • sb-otakustay[otakustay@gmail.com] 140 | 141 |
    142 | 143 | ::expect-useSimpleFilterArgTarget:: 144 | 145 |
      146 | 147 |
    • halfbrain-errorrik[errorrik@gmail.com] 148 | 149 |
    • halfbrain-otakustay[otakustay@gmail.com] 150 | 151 |
    152 | 153 | ::expect-useEngineTarget:: 154 | Engine(default)Engine() 155 | 156 | 157 | ::expect-useDepTarget:: 158 | 159 |
    header
    160 |
    default 161 |
    162 | 163 | ::eof:: 164 | -------------------------------------------------------------------------------- /test/spec/var.spec.js: -------------------------------------------------------------------------------- 1 | (function (){ 2 | 3 | var etpl = require( '../../src/main' ); 4 | var readText = require( './readTextSync' ); 5 | var text = readText( 'var.text.html' ); 6 | 7 | etpl.addFilter('var-emphasis', function (source, level) { 8 | level = level || 1; 9 | while (level--) { 10 | source = source + '!'; 11 | } 12 | 13 | return source; 14 | }); 15 | 16 | describe('Var', function() { 17 | it('can declare user variable', function() { 18 | var renderer = etpl.compile( text['tpl-simple'] ); 19 | expect(renderer()).toEqual(text['expect-simple']); 20 | }); 21 | 22 | it('can use filter in user variable assignment', function() { 23 | var renderer = etpl.compile( text['tpl-simple-filter'] ); 24 | expect(renderer()).toEqual(text['expect-simple-filter']); 25 | }); 26 | 27 | it('can use filter and pass arg in user variable assignment', function() { 28 | var renderer = etpl.compile( text['tpl-simple-filter-arg'] ); 29 | expect(renderer()).toEqual(text['expect-simple-filter-arg']); 30 | }); 31 | 32 | it('command literal allow break line', function() { 33 | var renderer = etpl.compile( text['tpl-breakline'] ); 34 | expect(renderer()).toEqual(text['expect-breakline']); 35 | }); 36 | 37 | it('has higher priority than data variable', function() { 38 | var data = { 39 | name: 'etpl' 40 | }; 41 | etpl.compile( text['tpl-prior'] ); 42 | expect(etpl.render('varPriorNoVarTarget', data)) 43 | .toEqual(text['expect-prior-novar']); 44 | expect(etpl.render('varPriorVarTarget', data)) 45 | .toEqual(text['expect-prior-var']); 46 | }); 47 | 48 | it('effective on declare position', function() { 49 | var data = { 50 | name: 'etpl' 51 | }; 52 | etpl.compile( text['tpl-ba'] ); 53 | expect(etpl.render('varBATarget', data)) 54 | .toEqual(text['expect-ba']); 55 | }); 56 | 57 | it('effective around the target', function() { 58 | etpl.compile( text['tpl-around'] ); 59 | expect(etpl.render('varAroundTarget')) 60 | .toEqual(text['expect-around']); 61 | }); 62 | 63 | it('can be read when data has getter method', function() { 64 | etpl.compile( text['tpl-data-getter'] ); 65 | expect(etpl.render('varDataGetterTarget', {get: function() {}})) 66 | .toEqual(text['expect-data-getter']); 67 | }); 68 | }); 69 | 70 | })(); -------------------------------------------------------------------------------- /test/spec/var.text.html: -------------------------------------------------------------------------------- 1 | ::tpl-simple:: 2 | 3 | Hello ${myVar}! 4 | ::expect-simple:: 5 | 6 | Hello etpl! 7 | ::tpl-simple-filter:: 8 | 9 | Hello ${myVar} 10 | ::expect-simple-filter:: 11 | 12 | Hello etpl! 13 | ::tpl-simple-filter-arg:: 14 | 15 | Hello ${myVar} 16 | ::expect-simple-filter-arg:: 17 | 18 | Hello etpl!!! 19 | ::tpl-breakline:: 20 | malefemale 24 | ::expect-breakline:: 25 | male 26 | ::tpl-prior:: 27 | Hello ${name}! 28 | Hello ${name}! 29 | 30 | ::expect-prior-novar:: 31 | Hello etpl! 32 | 33 | ::expect-prior-var:: 34 | Hello <b>etpl</b>! 35 | 36 | ::tpl-ba:: 37 | 38 | Hello ${name}! 39 | 40 | Hello ${name}! 41 | ::expect-ba:: 42 | 43 | Hello etpl! 44 | 45 | Hello <b>etpl</b>! 46 | ::tpl-around:: 47 | 48 | Hello ${name}! 49 | ::expect-around:: 50 | 51 | Hello etpl! 52 | ::tpl-data-getter:: 53 | 54 | Hello ${myVar}! 55 | ::expect-data-getter:: 56 | 57 | Hello etpl! 58 | ::eof:: -------------------------------------------------------------------------------- /test/spec/variableSubstitution.spec.js: -------------------------------------------------------------------------------- 1 | (function (){ 2 | 3 | var etpl = require( '../../src/main' ); 4 | var readText = require( './readTextSync' ); 5 | var text = readText( 'variableSubstitution.text.html' ); 6 | etpl.addFilter( 'vs-slice', function (source, begin, end) { 7 | return source.slice( begin, end ); 8 | } ); 9 | etpl.addFilter( 'vs-dateFormat', function (source) { 10 | return source.getFullYear() + '-' + (source.getMonth() + 1) + '-' + source.getDate(); 11 | } ); 12 | 13 | etpl.addFilter('vs-add', function (source) { 14 | for (var i = 1; i < arguments.length; i++) { 15 | source += arguments[i]; 16 | } 17 | 18 | return source; 19 | }); 20 | etpl.compile( text['tpl'] ); 21 | 22 | var data = { 23 | name: text['name'], 24 | address: text['address'], 25 | info: { 26 | repos: text['info-repos'], 27 | contributor: { 28 | name: text['info-contributor-name'], 29 | email: text['info-contributor-email'] 30 | } 31 | }, 32 | first: parseInt( text.first, 10 ), 33 | end: parseInt( text.end, 10 ), 34 | date: eval( text.date ) 35 | }; 36 | 37 | data.contributors = [data.info.contributor]; 38 | 39 | describe('Variable Substitution', function() { 40 | it( 41 | 'should encode html by default', 42 | function() { 43 | expect(etpl.getRenderer('variableSubstitution-normal')(data)) 44 | .toEqual(text['expect-normal']); 45 | } 46 | ); 47 | 48 | it( 49 | 'should substitute raw string when use raw filter', 50 | function() { 51 | expect(etpl.getRenderer('variableSubstitution-raw')(data)) 52 | .toEqual(text['expect-raw']); 53 | } 54 | ); 55 | 56 | it( 57 | 'should substitute correctly if has more than 1 filters', 58 | function() { 59 | expect(etpl.getRenderer('variableSubstitution-filters')(data)) 60 | .toEqual(text['expect-filters']); 61 | } 62 | ); 63 | 64 | it( 65 | 'should use raw value when prefix "*"', 66 | function() { 67 | expect(etpl.getRenderer('variableSubstitution-rawvalue')(data)) 68 | .toEqual(text['expect-rawvalue']); 69 | } 70 | ); 71 | 72 | it( 73 | 'can pass variable in filter', 74 | function() { 75 | expect(etpl.getRenderer('variableSubstitution-filter-arg')(data)) 76 | .toEqual(text['expect-filter-arg']); 77 | } 78 | ); 79 | 80 | it( 81 | 'can pass variable which use filter in filter', 82 | function() { 83 | expect(etpl.getRenderer('variableSubstitution-filter-filter')(data)) 84 | .toEqual(text['expect-filter-filter']); 85 | } 86 | ); 87 | 88 | it( 89 | 'should substitute correctly when many variables and filters mixed', 90 | function() { 91 | expect(etpl.getRenderer('variableSubstitution-mix')(data)) 92 | .toEqual(text['expect-mix']); 93 | } 94 | ); 95 | 96 | it( 97 | 'can use property accessor "."', 98 | function() { 99 | expect(etpl.getRenderer('variableSubstitution-property-accessor')(data)) 100 | .toEqual(text['expect-property-accessor']); 101 | } 102 | ); 103 | 104 | it( 105 | 'can use property accessor "[]"', 106 | function() { 107 | expect(etpl.getRenderer('variableSubstitution-property-accessor2')(data)) 108 | .toEqual(text['expect-property-accessor2']); 109 | } 110 | ); 111 | }); 112 | 113 | })(); -------------------------------------------------------------------------------- /test/spec/variableSubstitution.text.html: -------------------------------------------------------------------------------- 1 | ::name:: 2 | ecom&fe 3 | ::info-repos:: 4 | https://github.com/ecomfe/etpl 5 | ::info-contributor-name:: 6 | erik 7 | ::info-contributor-email:: 8 | errorrik@gmail.com 9 | ::address:: 10 | baidu 11 | ::first:: 12 | 0 13 | ::end:: 14 | 10 15 | ::date:: 16 | (function() {return new Date(2011, 10, 11);})() 17 | ::tpl:: 18 | 19 | Hello ${name|html|url}! 20 | 21 | Hello ${name}! 22 | 23 | Hello ${name|raw}! 24 | 25 | Date: ${*date|vs-dateFormat} 26 | 27 | Hello ${name | vs-slice( ${first}+3 + 6 - 2 - 4, ${end} - 3 + 5 - 2 ) }! 28 | 29 | Hello ${name | vs-slice( ${first | vs-add(${a}, ${a | vs-add(3, ${a})}) } - 3, ${end} ) }! 30 | 31 | Repos: ${info.repos} 32 | Contributor: ${info.contributor.name}[${info.contributor.email}] 33 | 34 | Repos: ${info["repos"]} 35 | Contributor: ${contributors[0].name}[${contributors[0]['email']}] 36 | Contributor: ${info['contributor'].name}[${info["contributor"]["email"]}] 37 | 38 | Hello ${name} in ${address}! Bye ${name}! Raw is ${name|raw}! 39 | 40 | ::expect-normal:: 41 | 42 | Hello <b>ecom&fe</b>! 43 | 44 | ::expect-raw:: 45 | 46 | Hello ecom&fe! 47 | 48 | ::expect-rawvalue:: 49 | 50 | Date: 2011-11-11 51 | 52 | ::expect-filters:: 53 | 54 | Hello %26lt%3Bb%26gt%3Becom%26amp%3Bfe%26lt%3B%2Fb%26gt%3B! 55 | 56 | ::expect-filter-arg:: 57 | 58 | Hello ecom&fe! 59 | 60 | ::expect-filter-filter:: 61 | 62 | Hello ecom&fe! 63 | 64 | ::expect-mix:: 65 | 66 | Hello <b>ecom&fe</b> in baidu! Bye <b>ecom&fe</b>! Raw is ecom&fe! 67 | 68 | ::expect-property-accessor:: 69 | 70 | Repos: https://github.com/ecomfe/etpl 71 | Contributor: erik[errorrik@gmail.com] 72 | 73 | ::expect-property-accessor2:: 74 | 75 | Repos: https://github.com/ecomfe/etpl 76 | Contributor: erik[errorrik@gmail.com] 77 | Contributor: erik[errorrik@gmail.com] 78 | 79 | ::eof:: -------------------------------------------------------------------------------- /tool/dist.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | TOOL_DIR=$(dirname "$0") 4 | PROJ_DIR="${TOOL_DIR}/.." 5 | 6 | cd "${PROJ_DIR}" 7 | rm -rf dist 8 | mkdir dist 9 | cp src/main.js dist/main.source.js 10 | cp src/tpl.js dist/tpl.source.js 11 | uglifyjs dist/main.source.js -mco dist/main.js 12 | uglifyjs dist/tpl.source.js -mco dist/tpl.js 13 | --------------------------------------------------------------------------------