/** * translate ast to js function code * @author yiminghe@gmail.com * @ignore */ KISSY.add("xtemplate/compiler", function (S, parser, ast, XTemplateRuntime, undefined) { parser.yy = ast; var doubleReg = /\\*"/g, singleReg = /\\*'/g, arrayPush = [].push, variableId = 0, xtemplateId = 0; function guid(str) { return str + (variableId++); } /** * @ignore */ function escapeString(str, isCode) { if (isCode) { str = escapeSingleQuoteInCodeString(str, false); } else { str = str.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); } str = str.replace(/\r/g, '\\r') .replace(/\n/g, '\\n').replace(/\t/g, '\\t'); return str; } function escapeSingleQuoteInCodeString(str, isDouble) { return str.replace(isDouble ? doubleReg : singleReg, function (m) { // \ 's number ,用户显式转过 "\'" , "\\\'" 就不处理了,否则手动对 ` 加 \ 转义 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('var buffer = ""' + (global ? ',' : ';')); if (global) { source.push('config = this.config,' + // current xtemplate engine 'engine = this, ' + 'utils = config.utils;'); var natives = '', c, utils = XTemplateRuntime.utils; for (c in utils) { natives += c + 'Util = 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', 'S', 'undefined'], source: source }; } }, genId: function (idNode, tplNode, preserveUndefined) { var source = [], depth = idNode.depth, idParts = idNode.parts, idName = guid('id'), self = this; // {{#each variable}} {{variable}} // {{command}} if (depth == 0) { var configNameCode = tplNode && self.genConfig(tplNode); var configName; if (configNameCode) { configName = configNameCode[0]; pushToArray(source, configNameCode[1]); } } // variable {{variable.subVariable}} var idString = self.getIdStringFromIdParts(source, idParts); source.push('var ' + idName + ' = getPropertyOrRunCommandUtil(engine,scopes,' + (configName || '{}') + ',"' + idString + '",' + depth + ',' + idNode.lineNumber + ',' + (tplNode && tplNode.escaped) + ',' + preserveUndefined + ');'); 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; }, genConfig: function (tplNode) { var source = [], configName, params, hash, self = this; if (tplNode) { params = tplNode.params; hash = tplNode.hash; if (params || hash) { configName = guid('config'); source.push('var ' + configName + ' = {};'); } if (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(configName + '.params=' + paramsName + ';'); } if (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(configName + '.hash=' + hashName + ';'); } } return [configName, 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, true) + "'"]]; }, 'number': function (e) { return ['', [e.value]]; }, 'boolean': function (e) { return ['', [e.value]]; }, 'id': function (e, topLevel) { // topLevel: {{n}} return this.genId(e, undefined, !topLevel); }, 'block': function (block) { var programNode = block.program, source = [], self = this, tplNode = block.tpl, configNameCode = self.genConfig(tplNode), configName = configNameCode[0], tplPath = tplNode.path, pathString = tplPath.string, inverseFn; pushToArray(source, configNameCode[1]); if (!configName) { configName = S.guid('config'); source.push('var ' + configName + ' = {};'); } source.push(configName + '.fn=' + self.genFunction(programNode.statements).join('\n') + ';'); if (programNode.inverse) { inverseFn = self.genFunction(programNode.inverse).join('\n'); source.push(configName + '.inverse=' + inverseFn + ';'); } // support {{^ // exchange fn with inverse if (tplNode.isInverted) { var tmp = guid('inverse'); source.push('var ' + tmp + '=' + configName + '.fn;'); source.push(configName + '.fn = ' + configName + '.inverse;'); source.push(configName + '.inverse = ' + tmp + ';'); } if (!tplNode.hash && !tplNode.params) { var parts = tplPath.parts; for (var i = 0; i < parts.length; i++) { // {{x[d]}} if (typeof parts[i] != 'string') { pathString = self.getIdStringFromIdParts(source, parts); break; } } } source.push('buffer += runBlockCommandUtil(engine, scopes, ' + configName + ', ' + '"' + pathString + '", ' + tplPath.lineNumber + ');'); return source; }, 'content': function (contentNode) { return ['buffer += \'' + escapeString(contentNode.value, false) + '\';']; }, 'tpl': function (tplNode) { var source = [], genIdCode = this.genId(tplNode.path, tplNode); pushToArray(source, genIdCode[1]); source.push('buffer += ' + genIdCode[0] + ';'); return source; }, 'tplExpression': function (e) { var source = [], escaped = e.escaped, expressionOrVariable; var code = this[e.expression.type](e.expression, 1); if (code[0]) { pushToArray(source, code[1]); expressionOrVariable = code[0]; } else { pushToArray(source, code[1].slice(0, -1)); expressionOrVariable = lastOfArray(code[1]); } source.push('buffer += getExpressionUtil(' + expressionOrVariable + ',' + escaped + ');'); return source; }, // consider x[d] 'getIdStringFromIdParts': function (source, idParts) { var idString = '', self = this, i, idPart, idPartType, nextIdNameCode, first = true; for (i = 0; i < idParts.length; i++) { idPart = idParts[i]; idPartType = idPart.type; if (!first) { idString += '.'; } if (idPartType) { nextIdNameCode = self[idPartType](idPart); if (nextIdNameCode[0]) { pushToArray(source, nextIdNameCode[1]); idString += '"+' + nextIdNameCode[0] + '+"'; first = true } } else { // number or string idString += idPart; first = false; } } return idString; } }; 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} config * @param {String} config.name template file name * @return {Function} */ compileToFn: function (tpl, config) { var code = compiler.compile(tpl); config = config || {}; var sourceURL = 'sourceURL=' + (config.name ? config.name : ('xtemplate' + (xtemplateId++))) + '.js'; // eval is not ok for eval("(function(){})") ie return Function.apply(null, [] .concat(code.params) .concat(code.source.join('\n') + // old chrome '\n//@ ' + sourceURL + // modern browser '\n//# ' + sourceURL)); } }; }, { requires: ['./compiler/parser', './compiler/ast', 'xtemplate/runtime'] }); /* todo: need oop, new Source().gen() */