diff --git a/test/browser/demo/async.js b/test/browser/demo/dep/async/0.0.0/async.js
similarity index 100%
rename from test/browser/demo/async.js
rename to test/browser/demo/dep/async/0.0.0/async.js
diff --git a/test/browser/demo/baidubce-sdk.bundle.js b/test/browser/demo/dep/baidubce-sdk/0.0.0/baidubce-sdk.bundle.js
similarity index 100%
rename from test/browser/demo/baidubce-sdk.bundle.js
rename to test/browser/demo/dep/baidubce-sdk/0.0.0/baidubce-sdk.bundle.js
diff --git a/test/browser/demo/dep/etpl/3.0.0.md5 b/test/browser/demo/dep/etpl/3.0.0.md5
new file mode 100644
index 0000000..f3ff9a4
--- /dev/null
+++ b/test/browser/demo/dep/etpl/3.0.0.md5
@@ -0,0 +1,11 @@
+{
+ "LICENSE": "5c597a93416a6b2fd475a4b6a5ae567f",
+ "README.md": "112e5f8aef658636f95f3829c0438d7a",
+ "doc/api.md": "3f730a37c3a97efe68b40981ae095d2b",
+ "doc/config.md": "8ebb6ec1a5d1a7c006ec7ac0371278d9",
+ "doc/syntax.md": "3c25984820a384c9149163f49846980b",
+ "main.js": "5c131f6789741ceba9117a95552d2918",
+ "package.json": "a0ec59a0969ab0b8baf493c5143689ac",
+ "src/main.js": "41a67fe16af3251b578f1bd0bd3f0b3b",
+ "src/tpl.js": "012520cf5f6e01fba5293ef27106b3ea"
+}
\ No newline at end of file
diff --git a/test/browser/demo/dep/etpl/3.0.0/.editorconfig b/test/browser/demo/dep/etpl/3.0.0/.editorconfig
new file mode 100644
index 0000000..be222b1
--- /dev/null
+++ b/test/browser/demo/dep/etpl/3.0.0/.editorconfig
@@ -0,0 +1,38 @@
+root = true
+
+[*]
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[**.js]
+indent_style = space
+indent_size = 4
+
+[**.css]
+indent_style = space
+indent_size = 4
+
+[**.less]
+indent_style = space
+indent_size = 4
+
+[**.styl]
+indent_style = space
+indent_size = 4
+
+[**.html]
+indent_style = space
+indent_size = 4
+
+[**.tpl]
+indent_style = space
+indent_size = 4
+
+[**.json]
+indent_style = space
+indent_size = 4
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/test/browser/demo/dep/etpl/3.0.0/.npmignore b/test/browser/demo/dep/etpl/3.0.0/.npmignore
new file mode 100644
index 0000000..32bcdf1
--- /dev/null
+++ b/test/browser/demo/dep/etpl/3.0.0/.npmignore
@@ -0,0 +1 @@
+/test
\ No newline at end of file
diff --git a/test/browser/demo/dep/etpl/3.0.0/.travis.yml b/test/browser/demo/dep/etpl/3.0.0/.travis.yml
new file mode 100644
index 0000000..a46b328
--- /dev/null
+++ b/test/browser/demo/dep/etpl/3.0.0/.travis.yml
@@ -0,0 +1,6 @@
+language: node_js
+node_js:
+ - 0.10
+branches:
+ only:
+ - master
\ No newline at end of file
diff --git a/test/browser/demo/dep/etpl/3.0.0/LICENSE b/test/browser/demo/dep/etpl/3.0.0/LICENSE
new file mode 100644
index 0000000..9723a0c
--- /dev/null
+++ b/test/browser/demo/dep/etpl/3.0.0/LICENSE
@@ -0,0 +1,28 @@
+Copyright (c) 2013, Baidu Inc.
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms, with or
+without modification, are permitted provided that the following conditions
+are met:
+
+Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+Neither the name of Baidu Inc. nor the names of its contributors may be used
+to endorse or promote products derived from this software without specific
+prior written permission of Baidu Inc.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/test/browser/demo/dep/etpl/3.0.0/README.md b/test/browser/demo/dep/etpl/3.0.0/README.md
new file mode 100644
index 0000000..0d89cb5
--- /dev/null
+++ b/test/browser/demo/dep/etpl/3.0.0/README.md
@@ -0,0 +1,117 @@
+# ETPL (Enterprise Template)
+
+[![Build Status](https://travis-ci.org/ecomfe/etpl.svg?branch=master)](https://travis-ci.org/ecomfe/etpl)
+
+ETPL是一个强复用、灵活、高性能的JavaScript模板引擎,适用于浏览器端或Node环境中视图的生成。
+
+
+## Start
+
+ETpl可以在`CommonJS/AMD`的模块定义环境中使用,也能直接在页面下通过`script`标签引用。
+
+
+### 浏览器环境
+
+直接通过script标签引用,你可以获得一个全局的`etpl`变量
+
+```html
+
+```
+
+在AMD环境的模块定义时,你可以通过`同步require`获得ETpl模块
+
+```javascript
+define(function (require) {
+ var etpl = require('etpl');
+});
+```
+
+在AMD环境,你也可以通过`异步require`获得ETpl模块
+
+```javascript
+require([ 'etpl' ], function (etpl) {
+});
+```
+
+*在AMD环境下,请确保你的require.config配置能够让Loader找到ETpl模块*
+
+### Node.JS环境
+
+你可以通过`npm`来安装ETpl
+
+```
+$ npm install etpl
+```
+
+安装完成后,你就可以通过`require`获得一个ETpl模块,正常地使用它
+
+```javascript
+var etpl = require('etpl');
+```
+
+### 使用
+
+使用ETPL模块,对模板源代码进行编译,会能得到编译后的function
+
+```javascript
+var render = etpl.compile('Hello ${name}!');
+```
+
+执行这个function,传入数据对象,就能得到模板执行的结果了
+
+```javascript
+var text = render({ name: 'etpl' });
+```
+
+查看更多例子,或者对模板渲染结果有疑虑,就去ETPL的[example](http://ecomfe.github.io/etpl/example.html)看看吧。
+
+
+## Documents
+
+通过文档,你可以更详细地了解ETpl的语法格式、使用方法、API等内容。
+
+- [模板语法](doc/syntax.md)
+- [API](doc/api.md)
+- [配置参数](doc/config.md)
+
+
+## Compatibility
+
+### ETpl3的新语法
+
+我们认为,当前流行的通过`block`来表达模板继承中的变化,是更好的表达方式。所以在ETpl3中,我们优化了母版的语法,删除了`master`、`contentplacehoder`、`content`标签,引入了`block`标签。
+
+对于ETpl2的使用者,我们提供一个[etpl2to3](https://github.com/ecomfe/etpl2to3)工具,能够帮助你平滑地将ETpl2的模板翻译成ETpl3。
+
+
+### get
+
+ETpl2中,为了前向兼容,Engine的`get`方法可以根据target名称获取模板内容。
+
+ETpl3不再支持该方法,所有的模板都通过render来使用:
+
+- 直接使用engine实例的render方法
+- 调用renderer function
+
+如果仍需要该功能,说明你正在维护一个遗留系统,并且没有很频繁的升级需求。请继续使用ETpl2。
+
+
+### merge
+
+ETpl的前身是[ER框架](https://github.com/ecomfe/er)自带的简易模板引擎,其基本与前身保持兼容。但出于代码体积和使用频度的考虑,ETpl删除了`merge`API。如果想要该API,请在自己的应用中加入如下代码:
+
+```javascript
+/**
+ * 执行模板渲染,并将渲染后的字符串作为innerHTML填充到HTML元素中。
+ * 兼容老版本的模板引擎api
+ *
+ * @param {HTMLElement} element 渲染字符串填充的HTML元素
+ * @param {string} name target名称
+ * @param {Object=} data 模板数据
+ */
+etpl.merge = function ( element, name, data ) {
+ if ( element ) {
+ element.innerHTML = this.render( name, data );
+ }
+};
+```
diff --git a/test/browser/demo/dep/etpl/3.0.0/main.js b/test/browser/demo/dep/etpl/3.0.0/main.js
new file mode 100644
index 0000000..f75835e
--- /dev/null
+++ b/test/browser/demo/dep/etpl/3.0.0/main.js
@@ -0,0 +1,9 @@
+/**
+ * ETPL (Enterprise Template)
+ * Copyright 2013 Baidu Inc. All rights reserved.
+ *
+ * @file Node入口
+ * @author firede(firede@firede.us)
+ */
+
+module.exports = require('./src/main');
diff --git a/test/browser/demo/dep/etpl/3.0.0/package.json b/test/browser/demo/dep/etpl/3.0.0/package.json
new file mode 100644
index 0000000..248ffe0
--- /dev/null
+++ b/test/browser/demo/dep/etpl/3.0.0/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "etpl",
+ "version": "3.0.0",
+ "contributors": [
+ { "name": "erik", "email": "errorrik@gmail.com" },
+ { "name": "otakustay", "email": "otakustay@gmail.com" },
+ { "name": "firede", "email": "firede@firede.us" }
+ ],
+ "main": "main",
+ "homepage": "http://ecomfe.github.io/etpl/",
+ "repository": "git://github.com/ecomfe/etpl",
+ "description": "ETPL是一个强复用、灵活、高性能的JavaScript模板引擎,适用于浏览器端或Node环境中视图的生成。",
+ "scripts": {
+ "test": "jasmine-node test/spec"
+ },
+ "devDependencies": {
+ "jasmine-node": "1.14.2"
+ }
+}
diff --git a/test/browser/demo/dep/etpl/3.0.0/src/main.js b/test/browser/demo/dep/etpl/3.0.0/src/main.js
new file mode 100644
index 0000000..0eb2afe
--- /dev/null
+++ b/test/browser/demo/dep/etpl/3.0.0/src/main.js
@@ -0,0 +1,1645 @@
+/**
+ * ETPL (Enterprise Template)
+ * Copyright 2013 Baidu Inc. All rights reserved.
+ *
+ * @file 模板引擎
+ * @author errorrik(errorrik@gmail.com)
+ * otakustay(otakustay@gmail.com)
+ */
+
+
+// HACK: 可见的重复代码未抽取成function和var是为了gzip size,吐槽的一边去
+
+(function (root) {
+ /**
+ * 对象属性拷贝
+ *
+ * @inner
+ * @param {Object} target 目标对象
+ * @param {Object} source 源对象
+ * @return {Object} 返回目标对象
+ */
+ function extend(target, source) {
+ for (var key in source) {
+ if (source.hasOwnProperty(key)) {
+ target[key] = source[key];
+ }
+ }
+
+ return target;
+ }
+
+ /**
+ * 随手写了个栈
+ *
+ * @inner
+ * @constructor
+ */
+ function Stack() {
+ this.raw = [];
+ this.length = 0;
+ }
+
+ Stack.prototype = {
+ /**
+ * 添加元素进栈
+ *
+ * @param {*} elem 添加项
+ */
+ push: function (elem) {
+ this.raw[this.length++] = elem;
+ },
+
+ /**
+ * 弹出顶部元素
+ *
+ * @return {*}
+ */
+ pop: function () {
+ if (this.length > 0) {
+ var elem = this.raw[--this.length];
+ this.raw.length = this.length;
+ return elem;
+ }
+ },
+
+ /**
+ * 获取顶部元素
+ *
+ * @return {*}
+ */
+ top: function () {
+ return this.raw[this.length - 1];
+ },
+
+ /**
+ * 获取底部元素
+ *
+ * @return {*}
+ */
+ bottom: function () {
+ return this.raw[0];
+ },
+
+ /**
+ * 根据查询条件获取元素
+ *
+ * @param {Function} condition 查询函数
+ * @return {*}
+ */
+ find: function (condition) {
+ var index = this.length;
+ while (index--) {
+ var item = this.raw[index];
+ if (condition(item)) {
+ return item;
+ }
+ }
+ }
+ };
+
+ /**
+ * 唯一id的起始值
+ *
+ * @inner
+ * @type {number}
+ */
+ var guidIndex = 0x2B845;
+
+ /**
+ * 获取唯一id,用于匿名target或编译代码的变量名生成
+ *
+ * @inner
+ * @return {string}
+ */
+ function generateGUID() {
+ return '___' + (guidIndex++);
+ }
+
+ /**
+ * 构建类之间的继承关系
+ *
+ * @inner
+ * @param {Function} subClass 子类函数
+ * @param {Function} superClass 父类函数
+ */
+ function inherits(subClass, superClass) {
+ /* jshint -W054 */
+ var F = new Function();
+ F.prototype = superClass.prototype;
+ subClass.prototype = new F();
+ subClass.prototype.constructor = subClass;
+ /* jshint +W054 */
+ // 由于引擎内部的使用场景都是inherits后,逐个编写子类的prototype方法
+ // 所以,不考虑将原有子类prototype缓存再逐个拷贝回去
+ }
+
+ /**
+ * HTML Filter替换的字符实体表
+ *
+ * @const
+ * @inner
+ * @type {Object}
+ */
+ var HTML_ENTITY = {
+ /* jshint ignore:start */
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ "'": '''
+ /* jshint ignore:end */
+ };
+
+ /**
+ * HTML Filter的替换函数
+ *
+ * @inner
+ * @param {string} c 替换字符
+ * @return {string}
+ */
+ function htmlFilterReplacer(c) {
+ return HTML_ENTITY[c];
+ }
+
+ /**
+ * 默认filter
+ *
+ * @inner
+ * @const
+ * @type {Object}
+ */
+ var DEFAULT_FILTERS = {
+ /**
+ * HTML转义filter
+ *
+ * @param {string} source 源串
+ * @return {string}
+ */
+ html: function (source) {
+ return source.replace(/[&<>"']/g, htmlFilterReplacer);
+ },
+
+ /**
+ * URL编码filter
+ *
+ * @param {string} source 源串
+ * @return {string}
+ */
+ url: encodeURIComponent,
+
+ /**
+ * 源串filter,用于在默认开启HTML转义时获取源串,不进行转义
+ *
+ * @param {string} source 源串
+ * @return {string}
+ */
+ raw: function (source) {
+ return source;
+ }
+ };
+
+ /**
+ * 字符串字面化
+ *
+ * @inner
+ * @param {string} source 需要字面化的字符串
+ * @return {string}
+ */
+ function stringLiteralize(source) {
+ return '"'
+ + source
+ .replace(/\x5C/g, '\\\\')
+ .replace(/"/g, '\\"')
+ .replace(/\x0A/g, '\\n')
+ .replace(/\x09/g, '\\t')
+ .replace(/\x0D/g, '\\r')
+ // .replace( /\x08/g, '\\b' )
+ // .replace( /\x0C/g, '\\f' )
+ + '"';
+ }
+
+ /**
+ * 对字符串进行可用于new RegExp的字面化
+ *
+ * @inner
+ * @param {string} source 需要字面化的字符串
+ * @return {string}
+ */
+ function regexpLiteral(source) {
+ return source.replace(/[\^\[\]\$\(\)\{\}\?\*\.\+]/g, function (c) {
+ return '\\' + c;
+ });
+ }
+
+ /**
+ * 字符串格式化
+ *
+ * @inner
+ * @param {string} source 目标模版字符串
+ * @param {...string} replacements 字符串替换项集合
+ * @return {string}
+ */
+ function stringFormat(source) {
+ var args = arguments;
+ return source.replace(
+ /\{([0-9]+)\}/g,
+ function (match, index) {
+ return args[index - 0 + 1];
+ });
+ }
+
+ /**
+ * 用于render的字符串变量声明语句
+ *
+ * @inner
+ * @const
+ * @type {string}
+ */
+ var RENDER_STRING_DECLATION = 'var r="";';
+
+ /**
+ * 用于render的字符串内容添加语句(起始)
+ *
+ * @inner
+ * @const
+ * @type {string}
+ */
+ var RENDER_STRING_ADD_START = 'r+=';
+
+ /**
+ * 用于render的字符串内容添加语句(结束)
+ *
+ * @inner
+ * @const
+ * @type {string}
+ */
+ var RENDER_STRING_ADD_END = ';';
+
+ /**
+ * 用于render的字符串内容返回语句
+ *
+ * @inner
+ * @const
+ * @type {string}
+ */
+ var RENDER_STRING_RETURN = 'return r;';
+
+ // HACK: IE8-时,编译后的renderer使用join Array的策略进行字符串拼接
+ if (typeof navigator !== 'undefined'
+ && /msie\s*([0-9]+)/i.test(navigator.userAgent)
+ && RegExp.$1 - 0 < 8
+ ) {
+ RENDER_STRING_DECLATION = 'var r=[],ri=0;';
+ RENDER_STRING_ADD_START = 'r[ri++]=';
+ RENDER_STRING_RETURN = 'return r.join("");';
+ }
+
+ /**
+ * 将访问变量名称转换成getVariable调用的编译语句
+ * 用于if、var等命令生成编译代码
+ *
+ * @inner
+ * @param {string} name 访问变量名
+ * @return {string}
+ */
+ function toGetVariableLiteral(name) {
+ name = name.replace(/^\s*\*/, '');
+ return stringFormat(
+ 'gv({0},["{1}"])',
+ stringLiteralize(name),
+ name.replace(
+ /\[['"]?([^'"]+)['"]?\]/g,
+ function (match, name) {
+ return '.' + name;
+ }
+ )
+ .split('.')
+ .join('","')
+ );
+ }
+
+ /**
+ * 解析文本片段中以固定字符串开头和结尾的包含块
+ * 用于 命令串: 和 变量替换串:${...} 的解析
+ *
+ * @inner
+ * @param {string} source 要解析的文本
+ * @param {string} open 包含块开头
+ * @param {string} close 包含块结束
+ * @param {boolean} greedy 是否贪婪匹配
+ * @param {function({string})} onInBlock 包含块内文本的处理函数
+ * @param {function({string})} onOutBlock 非包含块内文本的处理函数
+ */
+ function parseTextBlock(source, open, close, greedy, onInBlock, onOutBlock) {
+ var closeLen = close.length;
+ var texts = source.split(open);
+ var level = 0;
+ var buf = [];
+
+ for (var i = 0, len = texts.length; i < len; i++) {
+ var text = texts[i];
+
+ if (i) {
+ var openBegin = 1;
+ level++;
+ while (1) {
+ var closeIndex = text.indexOf(close);
+ if (closeIndex < 0) {
+ buf.push(level > 1 && openBegin ? open : '', text);
+ break;
+ }
+
+ level = greedy ? level - 1 : 0;
+ buf.push(
+ level > 0 && openBegin ? open : '',
+ text.slice(0, closeIndex),
+ level > 0 ? close : ''
+ );
+ text = text.slice(closeIndex + closeLen);
+ openBegin = 0;
+
+ if (level === 0) {
+ break;
+ }
+ }
+
+ if (level === 0) {
+ onInBlock(buf.join(''));
+ onOutBlock(text);
+ buf = [];
+ }
+ }
+ else {
+ text && onOutBlock(text);
+ }
+ }
+
+ if (level > 0 && buf.length > 0) {
+ onOutBlock(open);
+ onOutBlock(buf.join(''));
+ }
+ }
+
+ /**
+ * 编译变量访问和变量替换的代码
+ * 用于普通文本或if、var、filter等命令生成编译代码
+ *
+ * @inner
+ * @param {string} source 源代码
+ * @param {Engine} engine 引擎实例
+ * @param {boolean} forText 是否为输出文本的变量替换
+ * @return {string}
+ */
+ function compileVariable(source, engine, forText) {
+ var code = [];
+ var options = engine.options;
+
+ var toStringHead = '';
+ var toStringFoot = '';
+ var wrapHead = '';
+ var wrapFoot = '';
+
+ // 默认的filter,当forText模式时有效
+ var defaultFilter;
+
+ if (forText) {
+ toStringHead = 'ts(';
+ toStringFoot = ')';
+ wrapHead = RENDER_STRING_ADD_START;
+ wrapFoot = RENDER_STRING_ADD_END;
+ defaultFilter = options.defaultFilter;
+ }
+
+ parseTextBlock(
+ source, options.variableOpen, options.variableClose, 1,
+
+ function (text) {
+ // 加入默认filter
+ // 只有当处理forText时,需要加入默认filter
+ // 处理if/var/use等command时,不需要加入默认filter
+ if (forText && text.indexOf('|') < 0 && defaultFilter) {
+ text += '|' + defaultFilter;
+ }
+
+ // variableCode是一个gv调用,然后通过循环,在外面包filter的调用
+ // 形成filter["b"](filter["a"](gv(...)))
+ //
+ // 当forText模式,处理的是文本中的变量替换时
+ // 传递给filter的需要是字符串形式,所以gv外需要包一层ts调用
+ // 形成filter["b"](filter["a"](ts(gv(...))))
+ //
+ // 当variableName以*起始时,忽略ts调用,直接传递原值给filter
+ var filterCharIndex = text.indexOf('|');
+ var variableName = (
+ filterCharIndex > 0
+ ? text.slice(0, filterCharIndex)
+ : text
+ ).replace(/^\s+/, '').replace(/\s+$/, '');
+ var filterSource = filterCharIndex > 0
+ ? text.slice(filterCharIndex + 1)
+ : '';
+
+ var variableRawValue = variableName.indexOf('*') === 0;
+ var variableCode = [
+ variableRawValue ? '' : toStringHead,
+ toGetVariableLiteral(variableName),
+ variableRawValue ? '' : toStringFoot
+ ];
+
+ if (filterSource) {
+ filterSource = compileVariable(filterSource, engine);
+ var filterSegs = filterSource.split('|');
+ for (var i = 0, len = filterSegs.length; i < len; i++) {
+ var seg = filterSegs[i];
+
+ if (/^\s*([a-z0-9_-]+)(\((.*)\))?\s*$/i.test(seg)) {
+ variableCode.unshift('fs["' + RegExp.$1 + '"](');
+
+ if (RegExp.$3) {
+ variableCode.push(',', RegExp.$3);
+ }
+
+ variableCode.push(')');
+ }
+ }
+ }
+
+ code.push(
+ wrapHead,
+ variableCode.join(''),
+ wrapFoot
+ );
+ },
+
+ function (text) {
+ code.push(
+ wrapHead,
+ forText ? stringLiteralize(text) : text,
+ wrapFoot
+ );
+ }
+ );
+
+ return code.join('');
+ }
+
+ /**
+ * 文本节点类
+ *
+ * @inner
+ * @constructor
+ * @param {string} value 文本节点的内容文本
+ * @param {Engine} engine 引擎实例
+ */
+ function TextNode(value, engine) {
+ this.value = value;
+ this.engine = engine;
+ }
+
+ TextNode.prototype = {
+ /**
+ * 获取renderer body的生成代码
+ *
+ * @return {string}
+ */
+ getRendererBody: function () {
+ var value = this.value;
+ var options = this.engine.options;
+
+ if (!value
+ || (options.strip && /^\s*$/.test(value))
+ ) {
+ return '';
+ }
+
+ return compileVariable(value, this.engine, 1);
+ },
+
+ /**
+ * 复制节点的方法
+ *
+ * @return {TextNode}
+ */
+ clone: function () {
+ return this;
+ }
+ };
+
+ /**
+ * 命令节点类
+ *
+ * @inner
+ * @constructor
+ * @param {string} value 命令节点的value
+ * @param {Engine} engine 引擎实例
+ */
+ function Command(value, engine) {
+ this.value = value;
+ this.engine = engine;
+ this.children = [];
+ this.cloneProps = [];
+ }
+
+ Command.prototype = {
+ /**
+ * 添加子节点
+ *
+ * @param {TextNode|Command} node 子节点
+ */
+ addChild: function (node) {
+ this.children.push(node);
+ },
+
+ /**
+ * 节点open,解析开始
+ *
+ * @param {Object} context 语法分析环境对象
+ */
+ open: function (context) {
+ var parent = context.stack.top();
+ parent && parent.addChild(this);
+ context.stack.push(this);
+ },
+
+ /**
+ * 节点闭合,解析结束
+ *
+ * @param {Object} context 语法分析环境对象
+ */
+ close: function (context) {
+ if (context.stack.top() === this) {
+ context.stack.pop();
+ }
+ },
+
+ /**
+ * 获取renderer body的生成代码
+ *
+ * @return {string}
+ */
+ getRendererBody: function () {
+ var buf = [];
+ var children = this.children;
+ for (var i = 0; i < children.length; i++) {
+ buf.push(children[i].getRendererBody());
+ }
+
+ return buf.join('');
+ },
+
+ /**
+ * 复制节点的方法
+ *
+ * @return {Command}
+ */
+ clone: function () {
+ var node = new this.constructor(this.value, this.engine);
+ for (var i = 0, l = this.children.length; i < l; i++) {
+ node.addChild(this.children[i].clone());
+ }
+
+ for (var i = 0, l = this.cloneProps.length; i < l; i++) {
+ var prop = this.cloneProps[i];
+ node[prop] = this[prop];
+ }
+
+ return node;
+ }
+ };
+
+ /**
+ * 命令自动闭合
+ *
+ * @inner
+ * @param {Object} context 语法分析环境对象
+ * @param {Function=} CommandType 自闭合的节点类型
+ */
+ function autoCloseCommand(context, CommandType) {
+ var stack = context.stack;
+ var closeEnd = CommandType
+ ? stack.find(
+ function (item) {
+ return item instanceof CommandType;
+ }
+ )
+ : stack.bottom();
+
+ if (closeEnd) {
+ var node;
+
+ while ((node = stack.top()) !== closeEnd) {
+ /* jshint ignore:start */
+ // 如果节点对象不包含autoClose方法
+ // 则认为该节点不支持自动闭合,需要抛出错误
+ // for等节点不支持自动闭合
+ if (!node.autoClose) {
+ throw new Error(node.type + ' must be closed manually: ' + node.value);
+ }
+ /* jshint ignore:end */
+
+ node.autoClose(context);
+ }
+
+ closeEnd.close(context);
+ }
+
+ return closeEnd;
+ }
+
+ /**
+ * renderer body起始代码段
+ *
+ * @inner
+ * @const
+ * @type {string}
+ */
+ var RENDERER_BODY_START = ''
+ + 'data=data||{};'
+ + 'var v={},fs=engine.filters,hg=typeof data.get=="function",'
+ + 'gv=function(n,ps){'
+ + 'var p=ps[0],d=v[p];'
+ + 'if(d==null){'
+ + 'if(hg){return data.get(n);}'
+ + 'd=data[p];'
+ + '}'
+ + 'for(var i=1,l=ps.length;i= TargetState.APPLIED) {
+ return 1;
+ }
+
+ var blocks = this.blocks;
+
+ function replaceBlock(node) {
+ var children = node.children;
+
+ if (children instanceof Array) {
+ for (var i = 0, len = children.length; i < len; i++) {
+ var child = children[i];
+ if (child instanceof BlockCommand && blocks[child.name]) {
+ child = children[i] = blocks[child.name];
+ }
+
+ replaceBlock(child);
+ }
+ }
+ }
+
+ var master = this.engine.targets[masterName];
+ if (master && master.applyMaster(master.master)) {
+ this.children = master.clone().children;
+ replaceBlock(this);
+ this.state = TargetState.APPLIED;
+ return 1;
+ }
+ };
+
+ /**
+ * 判断target是否ready
+ * 包括是否成功应用母版,以及import语句依赖的target是否ready
+ *
+ * @return {boolean}
+ */
+ TargetCommand.prototype.isReady = function () {
+ if (this.state >= TargetState.READY) {
+ return 1;
+ }
+
+ var engine = this.engine;
+ var readyState = 1;
+
+ /**
+ * 递归检查节点的ready状态
+ *
+ * @inner
+ * @param {Command|TextNode} node 目标节点
+ */
+ function checkReadyState(node) {
+ for (var i = 0, len = node.children.length; i < len; i++) {
+ var child = node.children[i];
+ if (child instanceof ImportCommand) {
+ var target = engine.targets[child.name];
+ readyState = readyState
+ && target && target.isReady(engine);
+ }
+ else if (child instanceof Command) {
+ checkReadyState(child);
+ }
+ }
+ }
+
+ if (this.applyMaster(this.master)) {
+ checkReadyState(this);
+ readyState && (this.state = TargetState.READY);
+ return readyState;
+ }
+ };
+
+ /**
+ * 获取target的renderer函数
+ *
+ * @return {function(Object):string}
+ */
+ TargetCommand.prototype.getRenderer = function () {
+ if (this.renderer) {
+ return this.renderer;
+ }
+
+ if (this.isReady()) {
+ // console.log(this.name + ' ------------------');
+ // console.log(RENDERER_BODY_START + RENDER_STRING_DECLATION
+ // + this.getRendererBody()
+ // + RENDER_STRING_RETURN);
+
+ /* jshint -W054 */
+ var realRenderer = new Function(
+ 'data', 'engine',
+ [
+ RENDERER_BODY_START,
+ RENDER_STRING_DECLATION,
+ this.getRendererBody(),
+ RENDER_STRING_RETURN
+ ].join('\n')
+ );
+ /* jshint +W054 */
+
+ var engine = this.engine;
+ this.renderer = function (data) {
+ return realRenderer(data, engine);
+ };
+
+ return this.renderer;
+ }
+
+ return null;
+ };
+
+ /**
+ * 将target节点对象添加到语法分析环境中
+ *
+ * @inner
+ * @param {TargetCommand} target target节点对象
+ * @param {Object} context 语法分析环境对象
+ */
+ function addTargetToContext(target, context) {
+ context.target = target;
+
+ var engine = context.engine;
+ var name = target.name;
+
+ if (engine.targets[name]) {
+ switch (engine.options.namingConflict) {
+ /* jshint ignore:start */
+ case 'override':
+ engine.targets[name] = target;
+ context.targets.push(name);
+ case 'ignore':
+ break;
+ /* jshint ignore:end */
+ default:
+ throw new Error('Target exists: ' + name);
+ }
+ }
+ else {
+ engine.targets[name] = target;
+ context.targets.push(name);
+ }
+ }
+
+ /**
+ * target节点open,解析开始
+ *
+ * @param {Object} context 语法分析环境对象
+ */
+ TargetCommand.prototype.open = function (context) {
+ autoCloseCommand(context);
+ Command.prototype.open.call(this, context);
+ this.state = TargetState.READING;
+ addTargetToContext(this, context);
+ };
+
+ /**
+ * Var节点open,解析开始
+ *
+ * @param {Object} context 语法分析环境对象
+ */
+ VarCommand.prototype.open =
+
+ /**
+ * Use节点open,解析开始
+ *
+ * @param {Object} context 语法分析环境对象
+ */
+ UseCommand.prototype.open = function (context) {
+ context.stack.top().addChild(this);
+ };
+
+ /**
+ * Block节点open,解析开始
+ *
+ * @param {Object} context 语法分析环境对象
+ */
+ BlockCommand.prototype.open = function (context) {
+ Command.prototype.open.call(this, context);
+ (context.imp || context.target).blocks[this.name] = this;
+ };
+
+ /**
+ * elif节点open,解析开始
+ *
+ * @param {Object} context 语法分析环境对象
+ */
+ ElifCommand.prototype.open = function (context) {
+ var elseCommand = new ElseCommand();
+ elseCommand.open(context);
+
+ var ifCommand = autoCloseCommand(context, IfCommand);
+ ifCommand.addChild(this);
+ context.stack.push(this);
+ };
+
+ /**
+ * else节点open,解析开始
+ *
+ * @param {Object} context 语法分析环境对象
+ */
+ ElseCommand.prototype.open = function (context) {
+ var ifCommand = autoCloseCommand(context, IfCommand);
+ ifCommand.addChild(this);
+ context.stack.push(this);
+ };
+
+ /**
+ * import节点open,解析开始
+ *
+ * @param {Object} context 语法分析环境对象
+ */
+ ImportCommand.prototype.open = function (context) {
+ this.parent = context.stack.top();
+ this.target = context.target;
+ Command.prototype.open.call(this, context);
+ this.state = TargetState.READING;
+ context.imp = this;
+ };
+
+ /**
+ * 节点解析结束
+ * 由于use节点无需闭合,处理时不会入栈,所以将close置为空函数
+ *
+ * @param {Object} context 语法分析环境对象
+ */
+ UseCommand.prototype.close =
+
+ /**
+ * 节点解析结束
+ * 由于var节点无需闭合,处理时不会入栈,所以将close置为空函数
+ *
+ * @param {Object} context 语法分析环境对象
+ */
+ VarCommand.prototype.close = function () {};
+
+ /**
+ * 节点解析结束
+ *
+ * @param {Object} context 语法分析环境对象
+ */
+ ImportCommand.prototype.close = function (context) {
+ Command.prototype.close.call(this, context);
+ this.state = TargetState.READED;
+ context.imp = null;
+ };
+
+ /**
+ * 节点闭合,解析结束
+ *
+ * @param {Object} context 语法分析环境对象
+ */
+ TargetCommand.prototype.close = function (context) {
+ Command.prototype.close.call(this, context);
+ this.state = this.master ? TargetState.READED : TargetState.APPLIED;
+ context.target = null;
+ };
+
+ /**
+ * 节点自动闭合,解析结束
+ * ImportCommand的自动结束逻辑为,在其开始位置后马上结束
+ * 所以,其自动结束时children应赋予其所属的parent
+ *
+ * @param {Object} context 语法分析环境对象
+ */
+ ImportCommand.prototype.autoClose = function (context) {
+ // move children to parent
+ var parentChildren = this.parent.children;
+ parentChildren.push.apply(parentChildren, this.children);
+ this.children.length = 0;
+
+ // move blocks to target
+ for (var key in this.blocks) {
+ this.target.blocks[key] = this.blocks[key];
+ }
+ this.blocks = {};
+
+ // do close
+ this.close(context);
+ };
+
+ /**
+ * 节点open前的处理动作:节点不在target中时,自动创建匿名target
+ *
+ * @param {Object} context 语法分析环境对象
+ */
+ UseCommand.prototype.beforeOpen =
+
+ /**
+ * 节点open前的处理动作:节点不在target中时,自动创建匿名target
+ *
+ * @param {Object} context 语法分析环境对象
+ */
+ ImportCommand.prototype.beforeOpen =
+
+ /**
+ * 节点open前的处理动作:节点不在target中时,自动创建匿名target
+ *
+ * @param {Object} context 语法分析环境对象
+ */
+ VarCommand.prototype.beforeOpen =
+
+ /**
+ * 节点open前的处理动作:节点不在target中时,自动创建匿名target
+ *
+ * @param {Object} context 语法分析环境对象
+ */
+ ForCommand.prototype.beforeOpen =
+
+ /**
+ * 节点open前的处理动作:节点不在target中时,自动创建匿名target
+ *
+ * @param {Object} context 语法分析环境对象
+ */
+ FilterCommand.prototype.beforeOpen =
+
+ /**
+ * 节点open前的处理动作:节点不在target中时,自动创建匿名target
+ *
+ * @param {Object} context 语法分析环境对象
+ */
+ BlockCommand.prototype.beforeOpen =
+
+ /**
+ * 节点open前的处理动作:节点不在target中时,自动创建匿名target
+ *
+ * @param {Object} context 语法分析环境对象
+ */
+ IfCommand.prototype.beforeOpen =
+
+ /**
+ * 文本节点被添加到分析环境前的处理动作:节点不在target中时,自动创建匿名target
+ *
+ * @param {Object} context 语法分析环境对象
+ */
+ TextNode.prototype.beforeAdd = function (context) {
+ if (context.stack.bottom()) {
+ return;
+ }
+
+ var target = new TargetCommand(generateGUID(), context.engine);
+ target.open(context);
+ };
+
+ /**
+ * 获取renderer body的生成代码
+ *
+ * @return {string}
+ */
+ ImportCommand.prototype.getRendererBody = function () {
+ this.applyMaster(this.name);
+ return Command.prototype.getRendererBody.call(this);
+ };
+
+ /**
+ * 获取renderer body的生成代码
+ *
+ * @return {string}
+ */
+ UseCommand.prototype.getRendererBody = function () {
+ return stringFormat(
+ '{0}engine.render({2},{{3}}){1}',
+ RENDER_STRING_ADD_START,
+ RENDER_STRING_ADD_END,
+ stringLiteralize(this.name),
+ compileVariable(this.args, this.engine).replace(
+ /(^|,)\s*([a-z0-9_]+)\s*=/ig,
+ function (match, start, argName) {
+ return (start || '') + stringLiteralize(argName) + ':';
+ }
+ )
+ );
+ };
+
+ /**
+ * 获取renderer body的生成代码
+ *
+ * @return {string}
+ */
+ VarCommand.prototype.getRendererBody = function () {
+ if (this.expr) {
+ return stringFormat(
+ 'v[{0}]={1};',
+ stringLiteralize(this.name),
+ compileVariable(this.expr, this.engine)
+ );
+ }
+
+ return '';
+ };
+
+ /**
+ * 获取renderer body的生成代码
+ *
+ * @return {string}
+ */
+ IfCommand.prototype.getRendererBody = function () {
+ return stringFormat(
+ 'if({0}){{1}}',
+ compileVariable(this.value, this.engine),
+ Command.prototype.getRendererBody.call(this)
+ );
+ };
+
+ /**
+ * 获取renderer body的生成代码
+ *
+ * @return {string}
+ */
+ ElseCommand.prototype.getRendererBody = function () {
+ return stringFormat(
+ '}else{{0}',
+ Command.prototype.getRendererBody.call(this)
+ );
+ };
+
+ /**
+ * 获取renderer body的生成代码
+ *
+ * @return {string}
+ */
+ ForCommand.prototype.getRendererBody = function () {
+ return stringFormat(
+ /* jshint ignore:start */
+ ''
+ + 'var {0}={1};'
+ + 'if({0} instanceof Array)'
+ + 'for (var {4}=0,{5}={0}.length;{4}<{5};{4}++){v[{2}]={4};v[{3}]={0}[{4}];{6}}'
+ + 'else if(typeof {0}==="object")'
+ + 'for(var {4} in {0}){v[{2}]={4};v[{3}]={0}[{4}];{6}}',
+ /* jshint ignore:end */
+ generateGUID(),
+ compileVariable(this.list, this.engine),
+ stringLiteralize(this.index || generateGUID()),
+ stringLiteralize(this.item),
+ generateGUID(),
+ generateGUID(),
+ Command.prototype.getRendererBody.call(this)
+ );
+ };
+
+ /**
+ * 获取renderer body的生成代码
+ *
+ * @return {string}
+ */
+ FilterCommand.prototype.getRendererBody = function () {
+ var args = this.args;
+ return stringFormat(
+ '{2}fs[{5}]((function(){{0}{4}{1}})(){6}){3}',
+ RENDER_STRING_DECLATION,
+ RENDER_STRING_RETURN,
+ RENDER_STRING_ADD_START,
+ RENDER_STRING_ADD_END,
+ Command.prototype.getRendererBody.call(this),
+ stringLiteralize(this.name),
+ args ? ',' + compileVariable(args, this.engine) : ''
+ );
+ };
+
+ /**
+ * 命令类型集合
+ *
+ * @type {Object}
+ */
+ var commandTypes = {};
+
+ /**
+ * 添加命令类型
+ *
+ * @inner
+ * @param {string} name 命令名称
+ * @param {Function} Type 处理命令用到的类
+ */
+ function addCommandType(name, Type) {
+ commandTypes[name] = Type;
+ Type.prototype.type = name;
+ }
+
+ addCommandType('target', TargetCommand);
+ addCommandType('block', BlockCommand);
+ addCommandType('import', ImportCommand);
+ addCommandType('use', UseCommand);
+ addCommandType('var', VarCommand);
+ addCommandType('for', ForCommand);
+ addCommandType('if', IfCommand);
+ addCommandType('elif', ElifCommand);
+ addCommandType('else', ElseCommand);
+ addCommandType('filter', FilterCommand);
+
+
+ /**
+ * etpl引擎类
+ *
+ * @constructor
+ * @param {Object=} options 引擎参数
+ * @param {string=} options.commandOpen 命令语法起始串
+ * @param {string=} options.commandClose 命令语法结束串
+ * @param {string=} options.variableOpen 变量语法起始串
+ * @param {string=} options.variableClose 变量语法结束串
+ * @param {string=} options.defaultFilter 默认变量替换的filter
+ * @param {boolean=} options.strip 是否清除命令标签前后的空白字符
+ * @param {string=} options.namingConflict target名字冲突时的处理策略
+ */
+ function Engine(options) {
+ this.options = {
+ commandOpen: '',
+ commandSyntax: /^\s*(\/)?([a-z]+)\s*(?::([\s\S]*))?$/,
+ variableOpen: '${',
+ variableClose: '}',
+ defaultFilter: 'html'
+ };
+
+ this.config(options);
+ this.targets = {};
+ this.filters = extend({}, DEFAULT_FILTERS);
+ }
+
+ /**
+ * 配置引擎参数,设置的参数将被合并到现有参数中
+ *
+ * @param {Object} options 参数对象
+ * @param {string=} options.commandOpen 命令语法起始串
+ * @param {string=} options.commandClose 命令语法结束串
+ * @param {string=} options.variableOpen 变量语法起始串
+ * @param {string=} options.variableClose 变量语法结束串
+ * @param {string=} options.defaultFilter 默认变量替换的filter
+ * @param {boolean=} options.strip 是否清除命令标签前后的空白字符
+ * @param {string=} options.namingConflict target名字冲突时的处理策略
+ */
+ Engine.prototype.config = function (options) {
+ extend(this.options, options);
+ };
+
+ /**
+ * 解析模板并编译,返回第一个target编译后的renderer函数。
+ *
+ * @param {string} source 模板源代码
+ * @return {function(Object):string}
+ */
+ Engine.prototype.compile =
+
+ /**
+ * 解析模板并编译,返回第一个target编译后的renderer函数。
+ * 该方法的存在为了兼容老模板引擎
+ *
+ * @param {string} source 模板源代码
+ * @return {function(Object):string}
+ */
+ Engine.prototype.parse = function (source) {
+ if (source) {
+ var targetNames = parseSource(source, this);
+ if (targetNames.length) {
+ return this.targets[targetNames[0]].getRenderer();
+ }
+ }
+
+ /* jshint -W054 */
+ return new Function('return ""');
+ /* jshint +W054 */
+ };
+
+ /**
+ * 根据target名称获取编译后的renderer函数
+ *
+ * @param {string} name target名称
+ * @return {function(Object):string}
+ */
+ Engine.prototype.getRenderer = function (name) {
+ var target = this.targets[name];
+ if (target) {
+ return target.getRenderer();
+ }
+ };
+
+ /**
+ * 执行模板渲染,返回渲染后的字符串。
+ *
+ * @param {string} name target名称
+ * @param {Object=} data 模板数据。
+ * 可以是plain object,
+ * 也可以是带有 {string}get({string}name) 方法的对象
+ * @return {string}
+ */
+ Engine.prototype.render = function (name, data) {
+ var renderer = this.getRenderer(name);
+ if (renderer) {
+ return renderer(data);
+ }
+
+ return '';
+ };
+
+ /**
+ * 增加过滤器
+ *
+ * @param {string} name 过滤器名称
+ * @param {Function} filter 过滤函数
+ */
+ Engine.prototype.addFilter = function (name, filter) {
+ if (typeof filter === 'function') {
+ this.filters[name] = filter;
+ }
+ };
+
+ /**
+ * 解析源代码
+ *
+ * @inner
+ * @param {string} source 模板源代码
+ * @param {Engine} engine 引擎实例
+ * @return {Array} target名称列表
+ */
+ function parseSource(source, engine) {
+ var commandOpen = engine.options.commandOpen;
+ var commandClose = engine.options.commandClose;
+ var commandSyntax = engine.options.commandSyntax;
+
+ var stack = new Stack();
+ var analyseContext = {
+ engine: engine,
+ targets: [],
+ stack: stack,
+ target: null
+ };
+
+ // text节点内容缓冲区,用于合并多text
+ var textBuf = [];
+
+ /**
+ * 将缓冲区中的text节点内容写入
+ *
+ * @inner
+ */
+ function flushTextBuf() {
+ var text;
+ if (textBuf.length > 0 && (text = textBuf.join(''))) {
+ var textNode = new TextNode(text, engine);
+ textNode.beforeAdd(analyseContext);
+
+ stack.top().addChild(textNode);
+ textBuf = [];
+
+ if (engine.options.strip
+ && analyseContext.current instanceof Command
+ ) {
+ textNode.value = text.replace(/^[\x20\t\r]*\n/, '');
+ }
+ analyseContext.current = textNode;
+ }
+ }
+
+ var NodeType;
+
+ parseTextBlock(
+ source, commandOpen, commandClose, 0,
+
+ function (text) { // 内文本的处理函数
+ var match = commandSyntax.exec(text);
+
+ // 符合command规则,并且存在相应的Command类,说明是合法有含义的Command
+ // 否则,为不具有command含义的普通文本
+ if (match
+ && (NodeType = commandTypes[match[2].toLowerCase()])
+ && typeof NodeType === 'function'
+ ) {
+ // 先将缓冲区中的text节点内容写入
+ flushTextBuf();
+
+ var currentNode = analyseContext.current;
+ if (engine.options.strip && currentNode instanceof TextNode) {
+ currentNode.value = currentNode.value
+ .replace(/\r?\n[\x20\t]*$/, '\n');
+ }
+
+ if (match[1]) {
+ currentNode = autoCloseCommand(analyseContext, NodeType);
+ }
+ else {
+ currentNode = new NodeType(match[3], engine);
+ if (typeof currentNode.beforeOpen === 'function') {
+ currentNode.beforeOpen(analyseContext);
+ }
+ currentNode.open(analyseContext);
+ }
+
+ analyseContext.current = currentNode;
+ }
+ else if (!/^\s*\/\//.test(text)) {
+ // 如果不是模板注释,则作为普通文本,写入缓冲区
+ textBuf.push(commandOpen, text, commandClose);
+ }
+
+ NodeType = null;
+ },
+
+ function (text) { // 外,普通文本的处理函数
+ // 普通文本直接写入缓冲区
+ textBuf.push(text);
+ }
+ );
+
+
+ flushTextBuf(); // 将缓冲区中的text节点内容写入
+ autoCloseCommand(analyseContext);
+
+ return analyseContext.targets;
+ }
+
+ var etpl = new Engine();
+ etpl.Engine = Engine;
+
+ if (typeof exports === 'object' && typeof module === 'object') {
+ // For CommonJS
+ exports = module.exports = etpl;
+ }
+ else if (typeof define === 'function' && define.amd) {
+ // For AMD
+ define(etpl);
+ }
+ else {
+ // For
-
+
@@ -89,6 +57,19 @@
+
+
+
+
+ 名称 |
+ 类型 |
+ 大小 |
+ 时间 |
+
+
+
+
+