UNPKG

xtemplate

Version:

eXtensible Template Engine lib on browser and nodejs. support async control, inheritance, include, logic expression, custom function and more.

735 lines (659 loc) 23 kB
/** * translate ast to js function code * @author yiminghe@gmail.com * @ignore */ var util = require('./runtime').util; // codeTemplates --------------------------- start var TOP_DECLARATION = [ 'var t;', 'var root = tpl.root;' , 'var directAccess = tpl.directAccess;' , 'var pos = tpl.pos = {line:1};', // for performance does not record col 'var nativeCommands = root.nativeCommands;', 'var utils = root.utils;'].join('\n'); function chainedVariableRead(idParts, root) { var str = idParts.join('.'); var part0 = idParts[0]; return [ '(', '(t=(' + 'scope.' + (root ? 'root.' : '') + 'affix &&' + 'scope.' + (root ? 'root.' : '') + 'affix.' + part0 + ')) !== undefined?', (idParts.length > 1 ? 'scope.' + (root ? 'root.' : '') + 'affix.' + str : 't') + ':', 'scope.' + (root ? 'root.' : '') + 'data.' + str, ')' ].join(''); } var CALL_NATIVE_COMMAND = '{lhs} = {name}Command.call(tpl, scope, {option}, buffer);'; var CALL_CUSTOM_COMMAND = 'buffer = callCommandUtil(tpl, scope, {option}, buffer, [{idParts}]);'; var CALL_FUNCTION = '{lhs} = callFnUtil(tpl, scope, {option}, buffer, [{idParts}]);'; var CALL_FUNCTION_DEPTH = '{lhs} = callFnUtil(tpl, scope, {option}, buffer, [{idParts}], {depth});'; var SCOPE_RESOLVE = 'var {lhs} = scope.resolve([{idParts}]);'; var IF_AFFIX_DIRECT_SCOPE_RESOLVE_TOP = [ 'var {lhs} = directAccess ? {value} : scope.resolve([{idPartsArr}]);' ].join('\n'); var IF_AFFIX_DIRECT_SCOPE_RESOLVE = [ 'var {lhs} = {value};' ].join('\n'); var IF_AFFIX_DIRECT_ROOT_SCOPE_RESOLVE = 'var {lhs} = {value};'; var DIRECT_SCOPE_RESOLVE = [ 'var {lhs} = scope.data;' ].join('\n'); var DIRECT_ROOT_SCOPE_RESOLVE = 'var {lhs} = scope.root.data;'; var SCOPE_RESOLVE_DEPTH = 'var {lhs} = scope.resolve([{idParts}],{depth});'; var REQUIRE_MODULE = 'var {variable} = re' + 'quire("{name}");'; var CHANGE_REQUIRE_PARAM = '{option}.params[0] = {variable}.TPL_NAME;'; var CHECK_BUFFER = ['if({name} && {name}.isBuffer){', 'buffer = {name};', '{name} = undefined;', '}'].join('\n'); var FUNC = ['function {functionName}({params}){', '{body}', '}'].join('\n'); var SOURCE_URL = [ '', '//# sourceURL = {name}.js' ].join('\n'); var DECLARE_NATIVE_COMMANDS = 'var {name}Command = nativeCommands["{name}"];'; var DECLARE_UTILS = 'var {name}Util = utils["{name}"];'; var BUFFER_WRITE = 'buffer.write({value});'; var BUFFER_APPEND = 'buffer.append({value});'; var BUFFER_WRITE_ESCAPED = 'buffer.writeEscaped({value});'; var RETURN_BUFFER = 'return buffer;'; // codeTemplates ---------------------------- end var XTemplateRuntime = require('./runtime'); var parser = require('./compiler/parser'); parser.yy = require('./compiler/ast'); var nativeCode = []; var substitute = util.substitute; var each = util.each; var nativeCommands = XTemplateRuntime.nativeCommands; var nativeUtils = XTemplateRuntime.utils; var globals = { }; globals['undefined'] = globals['null'] = globals['true'] = globals['false'] = 1; each(nativeUtils, function (v, name) { nativeCode.push(substitute(DECLARE_UTILS, { name: name })); }); each(nativeCommands, function (v, name) { nativeCode.push(substitute(DECLARE_NATIVE_COMMANDS, { name: name })); }); nativeCode = nativeCode.join('\n'); var doubleReg = /\\*"/g, singleReg = /\\*'/g, arrayPush = [].push; function isGlobalId(node) { if (globals[node.string]) { return 1; } return 0; } function guid(self, str) { return str + (self.uuid++); } function wrapByDoubleQuote(str) { return '"' + str + '"'; } function wrapBySingleQuote(str) { return '\'' + str + '\''; } function joinArrayOfString(arr) { return wrapByDoubleQuote(arr.join('","')); } function escapeSingleQuoteInCodeString(str, isDouble) { return str.replace(isDouble ? doubleReg : singleReg, function (m) { // \ 's number ,用户显式转过 "\'" , "\\\'" 就不处理了,否则手动对 ` 加 \ 转义 if (m.length % 2) { m = '\\' + m; } return m; }); } function escapeString(str, isCode) { if (isCode) { str = escapeSingleQuoteInCodeString(str, 0); } else { /*jshint quotmark:false*/ str = str.replace(/\\/g, '\\\\') .replace(/'/g, "\\'"); } str = str.replace(/\r/g, '\\r') .replace(/\n/g, '\\n') .replace(/\t/g, '\\t'); return str; } function pushToArray(to, from) { arrayPush.apply(to, from); } function opExpression(e, extra) { var source = [], type = e.opType, exp1, exp2, code1Source, code2Source, code1 = this[e.op1.type](e.op1, extra), code2 = this[e.op2.type](e.op2, extra); exp1 = code1.exp; exp2 = code2.exp; var exp = guid(this, 'exp'); code1Source = code1.source; code2Source = code2.source; pushToArray(source, code1Source); source.push('var ' + exp + ' = ' + exp1 + ';'); if (type === '&&' || type === '||') { source.push('if(' + (type === '&&' ? '' : '!') + '(' + exp + ')){'); pushToArray(source, code2Source); source.push(exp + ' = ' + exp2 + ';'); source.push('}'); } else { pushToArray(source, code2Source); source.push(exp + ' = ' + '(' + exp1 + ')' + type + '(' + exp2 + ');'); } return { exp: exp, source: source }; } var lastLine = 1; function markPos(pos, source) { if (lastLine === pos.line) { return; } lastLine = pos.line; source.push('pos.line = ' + pos.line + ';'); } function isComplexIdParts(idParts) { var check = 0; var i, l; for (i = 0, l = idParts.length; i < l; i++) { if (idParts[i].type) { check = 1; break; } } return check; } // consider x[d] function getIdStringFromIdParts(self, source, idParts, extra) { if (idParts.length === 1) { return null; } var i, l, idPart, idPartType, check = isComplexIdParts(idParts), nextIdNameCode; if (check) { var ret = []; for (i = 0, l = idParts.length; i < l; i++) { idPart = idParts[i]; idPartType = idPart.type; if (idPartType) { nextIdNameCode = self[idPartType](idPart, extra); pushToArray(source, nextIdNameCode.source); ret.push(nextIdNameCode.exp); } else { // literal a.x ret.push(wrapByDoubleQuote(idPart)); } } return ret; } else { return null; } } function genFunction(self, statements) { var functionName = guid(self, 'func'); var source = ['function ' + functionName + '(scope, buffer) {']; var statement; for (var i = 0, len = statements.length; i < len; i++) { statement = statements[i]; pushToArray(source, self[statement.type](statement).source); } source.push(RETURN_BUFFER); source.push('}'); pushToArray(self.functionDeclares, source); return functionName; } function genTopFunction(self, statements, name) { var source = [ 'function run(tpl) {', TOP_DECLARATION, nativeCode ]; var statement; for (var i = 0, len = statements.length; i < len; i++) { statement = statements[i]; pushToArray(source, self[statement.type](statement, { top: 1 }).source); } source.splice.apply(source, [2, 0].concat(self.functionDeclares).concat('')); source.push(RETURN_BUFFER); source.push('}'); source.push('function tryRun(tpl) {'); source.push('try {'); source.push('return run(tpl);'); source.push('} catch(e) {'); source.push('if(!e.xtpl){'); source.push('e.message += " (' + escapeString(name) + ':"+tpl.pos.line+")"'); source.push('e.xtpl = {pos: tpl.pos, name: ' + wrapBySingleQuote(escapeString(name)) + '};'); source.push('buffer.error(e);'); source.push('}'); source.push('throw e;'); source.push('}'); source.push('}'); source.push('return tryRun(this);'); return { params: ['scope', 'buffer'], source: source.join('\n') }; } function genOptionFromFunction(self, func, escape, extra) { var optionName = guid(self, 'option'); var source = ['var ' + optionName + ' = {' + (escape ? 'escape: 1' : '') + '};'], params = func.params, hash = func.hash; if (params) { var paramsName = guid(self, 'params'); source.push('var ' + paramsName + ' = [];'); each(params, function (param) { var nextIdNameCode = self[param.type](param, extra); pushToArray(source, nextIdNameCode.source); source.push(paramsName + '.push(' + nextIdNameCode.exp + ');'); }); source.push(optionName + '.params = ' + paramsName + ';'); } if (hash) { var hashName = guid(self, 'hash'); source.push('var ' + hashName + ' = {};'); each(hash.value, function (v, key) { var nextIdNameCode = self[v.type](v, extra); pushToArray(source, nextIdNameCode.source); source.push(hashName + '[' + wrapByDoubleQuote(key) + '] = ' + nextIdNameCode.exp + ';'); }); source.push(optionName + '.hash = ' + hashName + ';'); } return { exp: optionName, source: source }; } function generateFunction(self, func, escape, block, extra) { var source = []; var functionConfigCode, optionName, idName; var id = func.id; var idString = id.string; var idParts = id.parts; var i; if (idString === 'elseif') { return { exp: '', source: [] }; } functionConfigCode = genOptionFromFunction(self, func, escape, extra); optionName = functionConfigCode.exp; pushToArray(source, functionConfigCode.source); if (block) { var programNode = block.program; var inverse = programNode.inverse; var elseIfs = []; var elseIf, functionValue, statement; var statements = programNode.statements; var thenStatements = []; for (i = 0; i < statements.length; i++) { statement = statements[i]; if (statement.type === 'expressionStatement' && (functionValue = statement.value) && functionValue.type === 'function' && functionValue.id.string === 'elseif') { if (elseIf) { elseIfs.push(elseIf); } elseIf = { condition: functionValue.params[0], statements: [] }; } else if (elseIf) { elseIf.statements.push(statement); } else { thenStatements.push(statement); } } if (elseIf) { elseIfs.push(elseIf); } // find elseIfs source.push(optionName + '.fn = ' + genFunction(self, thenStatements) + ';'); if (inverse) { source.push(optionName + '.inverse = ' + genFunction(self, inverse) + ';'); } if (elseIfs.length) { var elseIfsVariable = guid(self, 'elseIfs'); source.push('var ' + elseIfsVariable + ' = []'); for (i = 0; i < elseIfs.length; i++) { var elseIfStatement = elseIfs[i]; var elseIfVariable = guid(self, 'elseIf'); source.push('var ' + elseIfVariable + ' = {}'); var condition = elseIfStatement.condition; var conditionCode = self[condition.type](condition, extra); source.push(elseIfVariable + '.test = function(scope){'); pushToArray(source, conditionCode.source); source.push('return (' + conditionCode.exp + ');'); source.push('};'); source.push(elseIfVariable + '.fn = ' + genFunction(self, elseIfStatement.statements) + ';'); source.push(elseIfsVariable + '.push(' + elseIfVariable + ');'); } source.push(optionName + '.elseIfs = ' + elseIfsVariable + ';'); } } if (self.isModule) { // require include/extend modules if (idString === 'include' || idString === 'extend') { // prevent require parse... var moduleVariable = guid(self, 'module'); source.push(substitute(REQUIRE_MODULE, { name: func.params[0].value, variable: moduleVariable })); source.push(substitute(CHANGE_REQUIRE_PARAM, { option: optionName, variable: moduleVariable })); } } if (!block) { idName = guid(self, 'callRet'); source.push('var ' + idName); } if (idString in nativeCommands) { source.push(substitute(CALL_NATIVE_COMMAND, { lhs: block ? 'buffer' : idName, name: idString, option: optionName })); } else if (block) { source.push(substitute(CALL_CUSTOM_COMMAND, { option: optionName, idParts: joinArrayOfString(idParts) })); } else { // x.y(1,2) // {x:{y:function(a,b){}}} var newParts = getIdStringFromIdParts(self, source, idParts, extra); source.push(substitute(id.depth ? CALL_FUNCTION_DEPTH : CALL_FUNCTION, { lhs: idName, option: optionName, idParts: (newParts ? newParts.join(',') : joinArrayOfString(idParts)), depth: id.depth })); } if (idName) { source.push(substitute(CHECK_BUFFER, { name: idName })); } return { exp: idName, source: source }; } function AstToJSProcessor(isModule) { this.functionDeclares = []; this.isModule = isModule; this.uuid = 0; } AstToJSProcessor.prototype = { constructor: AstToJSProcessor, arrayExpression: function (e, extra) { var list = e.list; var len = list.length; var r; var source = []; var exp = []; for (var i = 0; i < len; i++) { r = this[list[i].type](list[i], extra); pushToArray(source, r.source); exp.push(r.exp); } return { exp: '[' + exp.join(',') + ']', source: source }; }, jsonExpression: function (e, extra) { var json = e.json; var len = json.length; var r; var source = []; var exp = []; for (var i = 0; i < len; i++) { var item = json[i]; r = this[item[1].type](item[1], extra); pushToArray(source, r.source); exp.push('"' + item[0] + '": ' + r.exp); } return { exp: '{' + exp.join(',') + '}', source: source }; }, conditionalOrExpression: opExpression, conditionalAndExpression: opExpression, relationalExpression: opExpression, equalityExpression: opExpression, additiveExpression: opExpression, multiplicativeExpression: opExpression, unaryExpression: function (e, extra) { var code = this[e.value.type](e.value, extra); return { exp: e.unaryType + '(' + code.exp + ')', source: code.source }; }, string: function (e) { // same as contentNode.value /*jshint quotmark:false*/ return { exp: wrapBySingleQuote(escapeString(e.value, 1)), source: [] }; }, number: function (e) { return { exp: e.value, source: [] }; }, id: function (idNode, extra) { var source = []; markPos(idNode.pos, source); if (isGlobalId(idNode)) { return { exp: idNode.string, source: source }; } var depth = idNode.depth; var idParts = idNode.parts; var idName = guid(this, 'id'); if (!depth) { var isComplex = isComplexIdParts(idParts); if (!isComplex) { var part0 = idParts[0]; var remain; var remainParts; if (part0 === 'this') { remainParts = idParts.slice(1); remain = remainParts.join('.'); if (remain) { remain = '.' + remain; } source.push(substitute(remain ? IF_AFFIX_DIRECT_SCOPE_RESOLVE : DIRECT_SCOPE_RESOLVE, { lhs: idName, value: chainedVariableRead(remainParts), idParts: remain })); return { exp: idName, source: source }; } else if (part0 === 'root') { remainParts = idParts.slice(1); remain = remainParts.join('.'); if (remain) { remain = '.' + remain; } source.push(substitute(remain ? IF_AFFIX_DIRECT_ROOT_SCOPE_RESOLVE : DIRECT_ROOT_SCOPE_RESOLVE, { lhs: idName, value: chainedVariableRead(remainParts, true), idParts: remain })); return { exp: idName, source: source }; } else if (extra && extra.top) { remain = idParts.join('.'); if (remain) { remain = '.' + remain; } source.push(substitute(IF_AFFIX_DIRECT_SCOPE_RESOLVE_TOP, { lhs: idName, idParts: remain, value: chainedVariableRead(idParts), idPartsArr: joinArrayOfString(idParts) })); return { exp: idName, source: source }; } } } // variable {{variable[subVariable]}} var newParts = getIdStringFromIdParts(this, source, idParts, extra); // optimize for x.y.z source.push(substitute(depth ? SCOPE_RESOLVE_DEPTH : SCOPE_RESOLVE, { lhs: idName, idParts: newParts ? newParts.join(',') : joinArrayOfString(idParts), depth: depth })); return { exp: idName, source: source }; }, 'function': function (func, extra) { return generateFunction(this, func, extra.escape, false, extra); }, blockStatement: function (block, extra) { return generateFunction(this, block.func, block.escape, block, extra); }, expressionStatement: function (expressionStatement, extra) { var source = [], escape = expressionStatement.escape, code, expression = expressionStatement.value, type = expression.type, expressionOrVariable; extra = extra || {}; extra.escape = escape; code = this[type](expression, extra); pushToArray(source, code.source); expressionOrVariable = code.exp; source.push(substitute(escape ? BUFFER_WRITE_ESCAPED : BUFFER_WRITE, { value: expressionOrVariable })); return { exp: '', source: source }; }, contentStatement: function (contentStatement) { /*jshint quotmark:false*/ return { exp: '', source: [ substitute(BUFFER_APPEND, { value: wrapBySingleQuote(escapeString(contentStatement.value, 0)) }) ] }; } }; var compiler; var anonymousCount = 0; /** * compiler for xtemplate * @class XTemplate.Compiler * @singleton */ compiler = { /** * get ast of template * @param {String} [name] xtemplate name * @param {String} tplContent * @return {Object} */ parse: function (tplContent, name) { return parser.parse(tplContent, name); }, /** * get template function string * @param {String} param.content * @param {String} [param.name] xtemplate name * @param {Boolean} [param.isModule] whether generated function is used in module * @return {String} */ compileToStr: function (param) { var func = compiler.compileToJson(param); return substitute(FUNC, { functionName: param.functionName || '', params: func.params.join(','), body: func.source }); }, /** * get template function json format * @param {String} [param.name] xtemplate name * @param {String} param.content * @param {Boolean} [param.isModule] whether generated function is used in module * @return {Object} */ compileToJson: function (param) { var name = param.name = param.name || ('xtemplate' + (++anonymousCount)); var root = compiler.parse(param.content, name); return genTopFunction(new AstToJSProcessor(param.isModule), root.statements, name); }, /** * get template function * @param {String} tplContent * @param {String} name template file name * @return {Function} */ compile: function (tplContent, name) { var code = compiler.compileToJson({ content: tplContent, name: name }); // eval is not ok for eval("(function(){})") ie return Function.apply(null, code.params .concat(code.source + substitute(SOURCE_URL, { name: name }))); } }; module.exports = compiler; /* todo: need oop, new Source().this() */