xtemplate
Version:
eXtensible Template Engine lib on browser and nodejs. support async control, inheritance, include, logic expression, custom function and more.
770 lines (769 loc) • 22.7 kB
JavaScript
var XTemplateRuntime = (function(){ var module = {};
var _xtemplateRuntime_;
_xtemplateRuntime_ = function (exports) {
/*
combined modules:
xtemplate/runtime
xtemplate/runtime/util
xtemplate/runtime/commands
xtemplate/runtime/scope
xtemplate/runtime/linked-buffer
*/
var xtemplateRuntimeUtil, xtemplateRuntimeScope, xtemplateRuntimeLinkedBuffer, xtemplateRuntimeCommands, xtemplateRuntime;
xtemplateRuntimeUtil = function (exports) {
// http://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet
// http://wonko.com/post/html-escaping
var htmlEntities = {
'&': '&',
'>': '>',
'<': '<',
'`': '`',
'/': '/',
'"': '"',
/*jshint quotmark:false*/
'\'': '''
};
var possibleEscapeHtmlReg = /[&<>"'`]/;
var escapeHtmlReg = getEscapeReg();
var SUBSTITUTE_REG = /\\?\{([^{}]+)\}/g;
var win = typeof global !== 'undefined' ? global : window;
function getEscapeReg() {
var str = '';
for (var entity in htmlEntities) {
str += entity + '|';
}
str = str.slice(0, -1);
escapeHtmlReg = new RegExp(str, 'g');
return escapeHtmlReg;
}
var util;
var toString = Object.prototype.toString;
exports = util = {
isArray: Array.isArray || function (obj) {
return toString.call(obj) === '[object Array]';
},
keys: Object.keys || function (o) {
var result = [];
var p;
for (p in o) {
if (o.hasOwnProperty(p)) {
result.push(p);
}
}
return result;
},
each: function (object, fn, context) {
if (object) {
var key, val, keys;
var i = 0;
var length = object && object.length;
var isObj = length === undefined || Object.prototype.toString.call(object) === '[object Function]';
context = context || null;
if (isObj) {
keys = util.keys(object);
for (; i < keys.length; i++) {
key = keys[i];
if (fn.call(context, object[key], key, object) === false) {
break;
}
}
} else {
for (val = object[0]; i < length; val = object[++i]) {
if (fn.call(context, val, i, object) === false) {
break;
}
}
}
}
return object;
},
mix: function (t, s) {
for (var p in s) {
t[p] = s[p];
}
return t;
},
globalEval: function (data) {
if (win.execScript) {
win.execScript(data);
} else {
(function (data) {
win['eval'].call(win, data);
}(data));
}
},
substitute: function (str, o, regexp) {
if (typeof str !== 'string' || !o) {
return str;
}
return str.replace(regexp || SUBSTITUTE_REG, function (match, name) {
if (match.charAt(0) === '\\') {
return match.slice(1);
}
return o[name] === undefined ? '' : o[name];
});
},
escapeHtml: function (str) {
str = '' + str;
if (!possibleEscapeHtmlReg.test(str)) {
return str;
}
return (str + '').replace(escapeHtmlReg, function (m) {
return htmlEntities[m];
});
},
log: function () {
if (typeof console !== 'undefined') {
console.log.apply(console, arguments);
}
}
};
return exports;
}();
xtemplateRuntimeScope = function (exports) {
function Scope(data) {
if (data !== undefined) {
this.data = data;
} else {
this.data = {};
}
this.root = this;
this.parent = undefined;
this.affix = undefined;
}
Scope.prototype = {
isScope: 1,
setParent: function (parentScope) {
this.parent = parentScope;
this.root = parentScope.root;
},
set: function (name, value) {
if (!this.affix) {
this.affix = {};
}
this.affix[name] = value;
},
setData: function (data) {
this.data = data;
},
getData: function () {
return this.data;
},
mix: function (v) {
var affix = this.affix;
if (!affix) {
affix = this.affix = {};
}
for (var name in v) {
affix[name] = v[name];
}
},
get: function (name) {
var data = this.data;
var v;
var affix = this.affix;
if (data != null) {
v = data[name];
}
if (v !== undefined) {
return v;
}
v = affix && affix[name];
return v;
},
resolve: function (parts, depth) {
var self = this;
var v;
if (!depth && parts.length === 1) {
v = self.get(parts[0]);
if (v !== undefined) {
return v;
} else {
depth = 1;
}
}
var len = parts.length;
var scope = self;
var i;
var part0 = parts[0];
if (depth) {
while (scope && depth--) {
scope = scope.parent;
}
}
if (!scope) {
return undefined;
}
if (part0 === 'this') {
v = scope.data;
} else if (part0 === 'root') {
scope = scope.root;
v = scope.data;
} else if (part0) {
do {
v = scope.get(part0);
} while (v === undefined && (scope = scope.parent));
} else {
return scope.data;
}
for (i = 1; v && i < len; i++) {
v = v[parts[i]];
}
return v;
}
};
exports = Scope;
return exports;
}();
xtemplateRuntimeLinkedBuffer = function (exports) {
var util = xtemplateRuntimeUtil;
function Buffer(list, next) {
this.list = list;
this.init();
this.next = next;
this.ready = false;
}
Buffer.prototype = {
constructor: Buffer,
isBuffer: 1,
init: function () {
this.data = '';
},
append: function (data) {
this.data += data;
return this;
},
write: function (data) {
if (data != null) {
this.append(data);
}
return this;
},
writeEscaped: function (data) {
if (data != null) {
this.append(util.escapeHtml(data));
}
return this;
},
async: function (fn) {
var self = this;
var list = self.list;
var nextFragment = new Buffer(list, self.next);
var asyncFragment = new Buffer(list, nextFragment);
self.next = asyncFragment;
self.ready = true;
fn(asyncFragment);
return nextFragment;
},
error: function (reason) {
var callback = this.list.callback;
if (callback) {
callback(reason, undefined);
this.list.callback = null;
}
},
end: function () {
var self = this;
if (self.list.callback) {
self.ready = true;
self.list.flush();
}
return self;
}
};
function LinkedBuffer(callback, config) {
var self = this;
self.config = config;
self.head = new Buffer(self, undefined);
self.callback = callback;
this.init();
}
LinkedBuffer.prototype = {
constructor: LinkedBuffer,
init: function () {
this.data = '';
},
append: function (data) {
this.data += data;
},
end: function () {
this.callback(null, this.data);
this.callback = null;
},
flush: function () {
var self = this;
var fragment = self.head;
while (fragment) {
if (fragment.ready) {
this.append(fragment.data);
} else {
self.head = fragment;
return;
}
fragment = fragment.next;
}
self.end();
}
};
LinkedBuffer.Buffer = Buffer;
exports = LinkedBuffer;
return exports;
}();
xtemplateRuntimeCommands = function (exports) {
var Scope = xtemplateRuntimeScope;
var util = xtemplateRuntimeUtil;
var commands = {
range: function (scope, option) {
var params = option.params;
var start = params[0];
var end = params[1];
var step = params[2];
if (!step) {
step = start > end ? -1 : 1;
} else if (start > end && step > 0 || start < end && step < 0) {
step = -step;
}
var ret = [];
for (var i = start; start < end ? i < end : i > end; i += step) {
ret.push(i);
}
return ret;
},
foreach: function (scope, option, buffer) {
var params = option.params;
var param0 = params[0];
var xindexName = params[2] || 'xindex';
var valueName = params[1];
var xcount, opScope, affix, xindex;
if (param0) {
xcount = param0.length;
for (xindex = 0; xindex < xcount; xindex++) {
opScope = new Scope(param0[xindex]);
affix = opScope.affix = {
xcount: xcount,
xindex: xindex
};
if (xindexName !== 'xindex') {
affix[xindexName] = xindex;
delete affix.xindex;
}
if (valueName) {
affix[valueName] = param0[xindex];
}
opScope.setParent(scope);
buffer = option.fn(opScope, buffer);
}
}
return buffer;
},
forin: function (scope, option, buffer) {
var params = option.params;
var param0 = params[0];
var xindexName = params[2] || 'xindex';
var valueName = params[1];
var opScope, affix, name;
if (param0) {
for (name in param0) {
opScope = new Scope(param0[name]);
affix = opScope.affix = {};
affix[xindexName] = name;
if (valueName) {
affix[valueName] = param0[name];
}
opScope.setParent(scope);
buffer = option.fn(opScope, buffer);
}
}
return buffer;
},
each: function (scope, option, buffer) {
var params = option.params;
var param0 = params[0];
if (param0) {
if (util.isArray(param0)) {
return commands.foreach(scope, option, buffer);
} else {
return commands.forin(scope, option, buffer);
}
}
return buffer;
},
'with': function (scope, option, buffer) {
var params = option.params;
var param0 = params[0];
if (param0) {
var opScope = new Scope(param0);
opScope.setParent(scope);
buffer = option.fn(opScope, buffer);
}
return buffer;
},
'if': function (scope, option, buffer) {
var params = option.params;
var param0 = params[0];
if (param0) {
var fn = option.fn;
if (fn) {
buffer = fn(scope, buffer);
}
} else {
var matchElseIf = false;
var elseIfs = option.elseIfs;
var inverse = option.inverse;
if (elseIfs) {
for (var i = 0, len = elseIfs.length; i < len; i++) {
var elseIf = elseIfs[i];
matchElseIf = elseIf.test(scope);
if (matchElseIf) {
buffer = elseIf.fn(scope, buffer);
break;
}
}
}
if (!matchElseIf && inverse) {
buffer = inverse(scope, buffer);
}
}
return buffer;
},
set: function (scope, option, buffer) {
scope.mix(option.hash);
return buffer;
},
include: function (scope, option, buffer) {
var params = option.params;
var i, newScope;
var l = params.length;
newScope = scope;
var directAccess = true;
if (option.hash) {
newScope = new Scope(option.hash);
newScope.setParent(scope);
directAccess = false;
}
for (i = 0; i < l; i++) {
buffer = this.root.include(params[i], this, newScope, option, buffer, directAccess);
}
return buffer;
},
parse: function (scope, option, buffer) {
return commands.include.call(this, new Scope(), option, buffer);
},
extend: function (scope, option, buffer) {
this.runtime.extendTplName = option.params[0];
return buffer;
},
block: function (scope, option, buffer) {
var self = this;
var runtime = self.runtime;
var params = option.params;
var blockName = params[0];
var type;
if (params.length === 2) {
type = params[0];
blockName = params[1];
}
var blocks = runtime.blocks = runtime.blocks || {};
var head = blocks[blockName], cursor;
var current = {
fn: option.fn,
type: type
};
if (!head) {
blocks[blockName] = current;
} else if (head.type) {
if (head.type === 'append') {
current.next = head;
blocks[blockName] = current;
} else if (head.type === 'prepend') {
var prev;
cursor = head;
while (cursor && cursor.type === 'prepend') {
prev = cursor;
cursor = cursor.next;
}
current.next = cursor;
prev.next = current;
}
}
if (!runtime.extendTplName) {
cursor = blocks[blockName];
while (cursor) {
if (cursor.fn) {
buffer = cursor.fn.call(self, scope, buffer);
}
cursor = cursor.next;
}
}
return buffer;
},
macro: function (scope, option, buffer) {
var hash = option.hash;
var params = option.params;
var macroName = params[0];
var params1 = params.slice(1);
var self = this;
var runtime = self.runtime;
var macros = runtime.macros = runtime.macros || {};
if (option.fn) {
macros[macroName] = {
paramNames: params1,
hash: hash,
fn: option.fn
};
} else {
var macro = macros[macroName];
var paramValues = macro.hash || {};
var paramNames;
if (macro && (paramNames = macro.paramNames)) {
for (var i = 0, len = paramNames.length; i < len; i++) {
var p = paramNames[i];
paramValues[p] = params1[i];
}
if (hash) {
for (var h in hash) {
paramValues[h] = hash[h];
}
}
var newScope = new Scope(paramValues);
buffer = macro.fn.call(self, newScope, buffer);
} else {
var error = 'in file: ' + self.name + ' can not find macro: ' + name + '" at line ' + self.pos.line + ', col ' + self.pos.col;
throw new Error(error);
}
}
return buffer;
}
};
commands['debugger'] = function () {
if ('@DEBUG@') {
util.globalEval('debugger');
}
};
exports = commands;
return exports;
}();
xtemplateRuntime = function (exports) {
var util = xtemplateRuntimeUtil;
var nativeCommands = xtemplateRuntimeCommands;
var commands = {};
var Scope = xtemplateRuntimeScope;
var LinkedBuffer = xtemplateRuntimeLinkedBuffer;
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 (extendTplName) {
runtime.extendTplName = null;
buffer = tpl.root.include(extendTplName, tpl, scope, null, buffer);
}
return buffer.end();
}
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);
});
}
};
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: '2.2.2',
nativeCommands: nativeCommands,
utils: utils,
util: util,
addCommand: function (commandName, fn) {
commands[commandName] = fn;
},
removeCommand: function (commandName) {
delete commands[commandName];
}
});
XTemplateRuntime.prototype = {
constructor: XTemplateRuntime,
Scope: Scope,
nativeCommands: nativeCommands,
utils: utils,
removeCommand: function (commandName) {
var config = this.config;
if (config.commands) {
delete config.commands[commandName];
}
},
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();
}
});
});
},
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;
exports = XTemplateRuntime;
return exports;
}();
exports = xtemplateRuntime;
return exports;
}();
return _xtemplateRuntime_;
})();