xtemplate
Version:
eXtensible Template Engine lib on browser and nodejs. support async control, inheritance, include, logic expression, custom function and more.
331 lines (299 loc) • 9.16 kB
JavaScript
/**
* xtemplate runtime
* @author yiminghe@gmail.com
* @ignore
*/
var util = require('./runtime/util');
var nativeCommands = require('./runtime/commands');
var commands = {};
var Scope = require('./runtime/scope');
var LinkedBuffer = require('./runtime/linked-buffer');
function findCommand(runtimeCommands, instanceCommands, parts) {
var name = parts[0];
var cmd = runtimeCommands && runtimeCommands[name] ||
instanceCommands && instanceCommands[name] ||
commands[name];
if (parts.length === 1) {
return cmd;
}
if (cmd) {
var len = parts.length;
for (var i = 1; i < len; i++) {
cmd = cmd[parts[i]];
if (!cmd) {
break;
}
}
}
return cmd;
}
function getSubNameFromParentName(parentName, subName) {
var parts = parentName.split('/');
var subParts = subName.split('/');
parts.pop();
for (var i = 0, l = subParts.length; i < l; i++) {
var subPart = subParts[i];
if (subPart === '.') {
} else if (subPart === '..') {
parts.pop();
} else {
parts.push(subPart);
}
}
return parts.join('/');
}
function renderTpl(tpl, scope, buffer) {
buffer = tpl.fn(scope, buffer);
var runtime = tpl.runtime;
var extendTplName = runtime.extendTplName;
// if has extend statement, only parse
if (extendTplName) {
runtime.extendTplName = null;
buffer = tpl.root.include(extendTplName, tpl, scope, null, buffer);
}
return buffer.end();
}
// depth: ../x.y() => 1
function callFn(tpl, scope, option, buffer, parts, depth) {
var error, caller, fn, command1;
if (!depth) {
command1 = findCommand(tpl.runtime.commands, tpl.root.config.commands, parts);
}
if (command1) {
return command1.call(tpl, scope, option, buffer);
} else {
error = 'in file: ' + tpl.name + ' can not call: ' + parts.join('.') + '" at line ' + tpl.pos.line + ', col ' + tpl.pos.col;
}
caller = scope.resolve(parts.slice(0, -1), depth);
fn = caller[parts[parts.length - 1]];
if (fn) {
return fn.apply(caller, option.params);
}
if (error) {
throw new Error(error);
}
return buffer;
}
var utils = {
callFn: callFn,
callCommand: function (tpl, scope, option, buffer, parts) {
return callFn(tpl, scope, option, buffer, parts);
}
};
var loader = {
cache: {},
load: function (params, callback) {
var name = params.name;
var cache = this.cache;
var cached = cache[name];
if (cached) {
return callback(undefined, cached);
}
require([name],
function (tpl) {
cache[name] = tpl;
callback(undefined, tpl);
},
function () {
var error = 'template "' + params.name + '" does not exist';
util.log(error, 'error');
callback(error);
}
);
}
};
/**
* template file name for chrome debug
*
* @cfg {Boolean} name
* @member XTemplate.Runtime
*/
/**
* XTemplate runtime. only accept tpl as function.
* @class XTemplate.Runtime
*/
function XTemplateRuntime(fn, config) {
var self = this;
self.fn = fn;
config = self.config = config || {};
config.loader = config.loader || XTemplateRuntime.loader;
this.subNameResolveCache = {};
}
util.mix(XTemplateRuntime, {
loader: loader,
version: '@VERSION@',
nativeCommands: nativeCommands,
utils: utils,
util: util,
/**
* add command to all template
* @method
* @static
* @param {String} commandName
* @param {Function} fn
* @member XTemplate.Runtime
*/
addCommand: function (commandName, fn) {
commands[commandName] = fn;
},
/**
* remove command from all template by name
* @method
* @static
* @param {String} commandName
* @member XTemplate.Runtime
*/
removeCommand: function (commandName) {
delete commands[commandName];
}
});
XTemplateRuntime.prototype = {
constructor: XTemplateRuntime,
Scope: Scope,
nativeCommands: nativeCommands,
utils: utils,
/**
* remove command by name
* @param commandName
*/
removeCommand: function (commandName) {
var config = this.config;
if (config.commands) {
delete config.commands[commandName];
}
},
/**
* add command definition to current template
* @param commandName
* @param {Function} fn command definition
*/
addCommand: function (commandName, fn) {
var config = this.config;
config.commands = config.commands || {};
config.commands[commandName] = fn;
},
resolve: function (subName, parentName) {
if (subName.charAt(0) !== '.') {
return subName;
}
if (!parentName) {
var error = 'parent template does not have name' +
' for relative sub tpl name: ' + subName;
throw new Error(error);
}
var key = parentName + '_ks_' + subName;
var nameResolveCache = this.subNameResolveCache;
var cached = nameResolveCache[key];
if (cached) {
return cached;
}
subName = nameResolveCache[key] = getSubNameFromParentName(parentName, subName);
return subName;
},
include: function (subTplName, tpl, scope, option, buffer, directAccess) {
var self = this;
var parentName = tpl.name;
var resolvedSubTplName = self.resolve(subTplName, parentName);
return buffer.async(function (newBuffer) {
self.config.loader.load({
root: self,
parentName: parentName,
originalName: subTplName,
name: resolvedSubTplName,
scope: scope,
option: option
}, function (error, tplFn) {
if (typeof tplFn === 'function') {
renderTpl({
directAccess: directAccess,
root: tpl.root,
fn: tplFn,
name: resolvedSubTplName,
runtime: tpl.runtime
}, scope, newBuffer);
} else if (error) {
newBuffer.error(error);
} else {
if (option && option.escaped) {
newBuffer.writeEscaped(tplFn);
} else {
newBuffer.append(tplFn);
}
newBuffer.end();
}
});
});
},
/**
* get result by merge data with template
* @param data
* @param option
* @param callback function called
* @return {String}
*/
render: function (data, option, callback) {
var html = '';
var self = this;
var fn = self.fn;
if (typeof option === 'function') {
callback = option;
option = null;
}
option = option || {};
callback = callback || function (error, ret) {
if (error) {
if (!(error instanceof Error)) {
error = new Error(error);
}
throw error;
}
html = ret;
};
var name = self.config.name;
if (!name && fn.TPL_NAME) {
name = fn.TPL_NAME;
}
var scope = new Scope(data);
var buffer = new XTemplateRuntime.LinkedBuffer(callback, self.config).head;
renderTpl({
name: name,
fn: fn,
runtime: {
commands: option.commands
},
root: self,
directAccess: true
}, scope, buffer);
return html;
}
};
XTemplateRuntime.Scope = Scope;
XTemplateRuntime.LinkedBuffer = LinkedBuffer;
module.exports = XTemplateRuntime;
/**
* @ignore
*
* 2012-09-12 yiminghe@gmail.com
* - 参考 velocity, 扩充 ast
* - Expression/ConditionalOrExpression
* - EqualityExpression/RelationalExpression...
*
* 2012-09-11 yiminghe@gmail.com
* - 初步完成,添加 tc
*
* 对比 template
*
* 优势
* - 不会莫名其妙报错(with)
* - 更多出错信息,直接给出行号
* - 更容易扩展 command, sub-tpl
* - 支持子模板
* - 支持作用域链: ..\x ..\..\y
* - 内置 escapeHtml 支持
* - 支持预编译
* - 支持简单表达式 +-/%* ()
* - 支持简单比较 === !===
* - 支持类似函数的嵌套命令
* 劣势
* - 不支持完整 js 语法
*/