/** * translate ast to js function code * @author yiminghe@gmail.com * @ignore */ KISSY.add("xtemplate/compiler", function (S, parser, ast, XTemplateRuntime) { 'use strict'; parser.yy = ast; var utils = { 'getProperty': 1 }, doubleReg = /\\*"/g, singleReg = /\\*'/g, arrayPush = [].push, variableId = 0, xtemplateId = 0; function guid(str) { return str + (variableId++); } // consider str compiler XTemplateRuntime.includeCommand.invokeEngine = function (tpl, scopes, option) { if (typeof tpl == 'string') { tpl = compiler.compileToFn(/** @type String @ignore */tpl, option); } return new XTemplateRuntime(tpl, S.merge(option)).render(scopes); }; /** * @ignore */ function escapeString(str, isDouble) { return escapeSingleQuoteInCodeString(str//.replace(/\\/g, '\\\\') .replace(/\r/g, '\\r') .replace(/\n/g, '\\n') .replace(/\t/g, '\\t'), isDouble); } function escapeSingleQuoteInCodeString(str, isDouble) { return str.replace(isDouble ? doubleReg : singleReg, function (m) { // \ 奇数,用户显式转过 "\'" , "\\\'" 就不处理了,否则手动对 ` 加 \ 转义 if (m.length % 2) { m = '\\' + m; } return m; }); } function pushToArray(to, from) { arrayPush.apply(to, from); } function lastOfArray(arr) { return arr[arr.length - 1]; } var gen = { // ------------ helper generation function start genFunction: function (statements, global) { var source = []; if (!global) { source.push('function(scopes) {'); } source.push('"use strict";'); source.push('var buffer = ""' + (global ? ',' : ';')); if (global) { source.push('S = KISSY,' + 'escapeHTML = S.escapeHTML,' + 'isArray = S.isArray,' + 'isObject = S.isObject,' + 'log = S.log,' + 'commands = option.commands,' + 'utils = option.utils,' + 'error = S.error;'); var natives = '', c, // shortcut for global commands commands = XTemplateRuntime.commands; for (c in commands) { natives += c + 'Command = commands["' + c + '"],'; } for (c in utils) { natives += c + ' = utils["' + c + '"],'; } if (natives) { source.push('var ' + natives.slice(0, natives.length - 1)); } } if (statements) { for (var i = 0, len = statements.length; i < len; i++) { pushToArray(source, this[statements[i].type](statements[i])); } } source.push('return buffer;'); if (!global) { source.push('}'); return source; } else { return { params: ['scopes', 'option', 'undefined'], source: source }; } }, genId: function (idNode, tplNode) { var source = [], depth = idNode.depth, idString = idNode.string, idName = guid('id'), self = this, foundNativeRuntimeCommand = 0, tmpNameCommand, commands = XTemplateRuntime.commands; source.push('var ' + idName + ';'); // {{#each variable}} {{variable}} if (tplNode && depth == 0) { var optionNameCode = self.genOption(tplNode); pushToArray(source, optionNameCode[1]); // skip if for global commands before current template's render if (foundNativeRuntimeCommand = commands[idString]) { tmpNameCommand = idString + 'Command'; } else { tmpNameCommand = guid('command'); source.push('var ' + tmpNameCommand + ';'); source.push(tmpNameCommand + ' = commands["' + idString + '"];'); source.push('if( ' + tmpNameCommand + ' ){'); } source.push('try{'); source.push(idName + ' = ' + tmpNameCommand + '(scopes,' + optionNameCode[0] + ');'); source.push('}catch(e){'); source.push('error(e.message+": \'' + idString + '\' at line ' + idNode.lineNumber + '");'); source.push('}'); if (!foundNativeRuntimeCommand) { source.push('}'); source.push('else {'); } } // variable {{variable.subVariable}} if (!foundNativeRuntimeCommand) { var tmp = guid('tmp'); source.push('var ' + tmp + '=getProperty("' + idString + '",scopes);'); source.push('if(' + tmp + '===false){'); source.push('S[option.silent?"log":"error"]("can not find property: \'' + idString + '\' at line ' + idNode.lineNumber + '", "warn");'); // only normalize when render // source.push(idName + ' = "";'); source.push('} else {'); source.push(idName + ' = ' + tmp + '[0];'); source.push('}'); if (tplNode && depth == 0) { source.push('}'); } } return [idName, source]; }, genOpExpression: function (e, type) { var source = [], name1, name2, code1 = this[e.op1.type](e.op1), code2 = this[e.op2.type](e.op2); name1 = code1[0]; name2 = code2[0]; if (name1 && name2) { pushToArray(source, code1[1]); pushToArray(source, code2[1]); source.push(name1 + type + name2); return ['', source]; } if (!name1 && !name2) { pushToArray(source, code1[1].slice(0, -1)); pushToArray(source, code2[1].slice(0, -1)); source.push('(' + lastOfArray(code1[1]) + ')' + type + '(' + lastOfArray(code2[1]) + ')'); return ['', source]; } if (name1 && !name2) { pushToArray(source, code1[1]); pushToArray(source, code2[1].slice(0, -1)); source.push(name1 + type + '(' + lastOfArray(code2[1]) + ')'); return ['', source]; } if (!name1 && name2) { pushToArray(source, code1[1].slice(0, -1)); pushToArray(source, code2[1]); source.push('(' + lastOfArray(code1[1]) + ')' + type + name2); return ['', source]; } return undefined; }, genOption: function (tplNode) { var source = [], optionName = guid('option'), params, hash, self = this; source.push('var ' + optionName + ' = S.merge(option);'); if (tplNode) { if (params = tplNode.params) { var paramsName = guid('params'); source.push('var ' + paramsName + ' = [];'); S.each(params, function (param) { var nextIdNameCode = self[param.type](param); if (nextIdNameCode[0]) { pushToArray(source, nextIdNameCode[1]); source.push(paramsName + '.push(' + nextIdNameCode[0] + ');') } else { pushToArray(source, nextIdNameCode[1].slice(0, -1)); source.push(paramsName + '.push(' + lastOfArray(nextIdNameCode[1]) + ');') } }); source.push(optionName + '.params=' + paramsName + ';'); } if (hash = tplNode.hash) { var hashName = guid('hash'); source.push('var ' + hashName + ' = {};'); S.each(hash.value, function (v, key) { var nextIdNameCode = self[v.type](v); if (nextIdNameCode[0]) { pushToArray(source, nextIdNameCode[1]); source.push(hashName + '["' + key + '"] = ' + nextIdNameCode[0] + ';') } else { pushToArray(source, nextIdNameCode[1].slice(0, -1)); source.push(hashName + '["' + key + '"] = ' + lastOfArray(nextIdNameCode[1]) + ';') } }); source.push(optionName + '.hash=' + hashName + ';'); } } return [optionName, source]; }, // ------------ helper generation function end conditionalOrExpression: function (e) { return this.genOpExpression(e, '||'); }, conditionalAndExpression: function (e) { return this.genOpExpression(e, '&&'); }, relationalExpression: function (e) { return this.genOpExpression(e, e.opType); }, equalityExpression: function (e) { return this.genOpExpression(e, e.opType); }, additiveExpression: function (e) { return this.genOpExpression(e, e.opType); }, multiplicativeExpression: function (e) { return this.genOpExpression(e, e.opType); }, unaryExpression: function (e) { var source = [], name, code = this[e.value.type](e.value); arrayPush.apply(source, code[1]); if (name = code[0]) { source.push(name + '=!' + name + ';'); } else { source[source.length - 1] = '!' + lastOfArray(source); } return [name, source]; }, 'string': function (e) { // same as contentNode.value return ['', ["'" + escapeString(e.value) + "'"]]; }, 'number': function (e) { return ['', [e.value]]; }, 'boolean': function (e) { return ['', [e.value]]; }, 'id': function (e) { return this.genId(e); }, 'block': function (block) { var programNode = block.program, source = [], self = this, tmpNameCommand, tplNode = block.tpl, optionNameCode = self.genOption(tplNode), optionName = optionNameCode[0], commands = XTemplateRuntime.commands, string = tplNode.path.string, inverseFn, tmp, existsNativeCommand, variableName; pushToArray(source, optionNameCode[1]); source.push(optionName + '.fn=' + self.genFunction(programNode.statements).join('\n') + ';'); if (programNode.inverse) { inverseFn = self.genFunction(programNode.inverse).join('\n'); source.push(optionName + '.inverse=' + inverseFn + ';'); } // reduce generated code size if (existsNativeCommand = commands[string]) { tmpNameCommand = string + 'Command'; } else { tmpNameCommand = guid('command'); source.push('var ' + tmpNameCommand + ' = commands["' + string + '"];'); // {{#xx}}1{#xx} => xx is not command => // if xx => array => {{#each xx}}1{/each}} // if xx => object => {{#with xx}}1{/with}} // else => {{#if xx}}1{/if}} if (!tplNode.hash && !tplNode.params) { source.push('if(!' + tmpNameCommand + '){'); tmp = guid('tmp'); source.push('var ' + tmp + ' = getProperty("' + string + '",scopes);'); source.push('if(' + tmp + ' !== false){'); variableName = guid('tmp'); source.push('var ' + variableName + '=' + tmp + '[0];'); source.push(optionName + '.params=[' + variableName + '];'); source.push('if(isArray(' + variableName + ')){'); source.push(tmpNameCommand + '=commands["each"];'); source.push('}'); source.push('else if(isObject(' + variableName + ')){'); source.push(tmpNameCommand + '=commands["with"];'); source.push('}'); source.push('else {'); source.push(tmpNameCommand + '=commands["if"];'); source.push('}'); source.push('}'); source.push('}'); } source.push('if( ' + tmpNameCommand + ' ){'); } source.push('try{'); source.push('buffer += ' + tmpNameCommand + '(scopes,' + optionName + ');'); source.push('}catch(e){'); source.push('error(e.message+": \'' + string + '\' at line ' + tplNode.path.lineNumber + '");'); source.push('}'); if (!existsNativeCommand) { source.push('}'); source.push('else {'); source.push('S[option.silent?"log":"error"]("can not find command: \'' + string + '\' at line ' + tplNode.path.lineNumber + '");'); source.push('}'); } return source; }, 'content': function (contentNode) { return ['buffer += \'' + escapeString(contentNode.value) + '\';']; }, 'tpl': function (tplNode) { var source = [], escaped = tplNode.escaped, genIdCode = this.genId(tplNode.path, tplNode); pushToArray(source, genIdCode[1]); outputVariable(genIdCode[0], escaped, source); return source; }, tplExpression: function (e) { var source = [], escaped = e.escaped, expressionOrVariable, code = this[e.expression.type](e.expression); if (code[0]) { pushToArray(source, code[1]); expressionOrVariable = code[0]; } else { pushToArray(source, code[1].slice(0, -1)); expressionOrVariable = lastOfArray(code[1]); } outputVariable(expressionOrVariable, escaped, source); return source; } }; function outputVariable(expressionOrVariable, escaped, source) { var tmp = guid('tmp'); // in case it is expression, avoid duplicate computation source.push('var ' + tmp + ' = ' + expressionOrVariable + ';'); source.push('buffer+=' + (escaped ? 'escapeHTML(' : '') + // when render undefined => '' '(' + tmp + '===undefined?"":' + tmp + ')' + '+""' + (escaped ? ')' : '') + ';'); } var compiler; /** * compiler for xtemplate * @class KISSY.XTemplate.compiler * @singleton */ return compiler = { /** * get ast of template * @param {String} tpl * @return {Object} */ parse: function (tpl) { return parser.parse(tpl); }, /** * get template function string * @param {String} tpl * @return {String} */ compileToStr: function (tpl) { var func = this.compile(tpl); return 'function(' + func.params.join(',') + '){\n' + func.source.join('\n') + '}'; }, /** * get template function json format * @param {String} tpl * @return {Object} */ compile: function (tpl) { var root = this.parse(tpl); variableId = 0; return gen.genFunction(root.statements, true); }, /** * get template function * @param {String} tpl * @param {Object} option * @param {String} option.name template file name * @return {Function} */ compileToFn: function (tpl, option) { var code = compiler.compile(tpl); option = option || {}; // eval is not ok for eval("(function(){})") ie return (Function.apply(null, [] .concat(code.params) .concat(code.source.join('\n') + '//@ sourceURL=' + (option.name ? option.name : ('xtemplate' + (xtemplateId++))) + '.js'))); } }; }, { requires: ['./compiler/parser', './compiler/ast', 'xtemplate/runtime'] });