UNPKG

xtemplate

Version:
592 lines (525 loc) 17.5 kB
/** * translate ast to js function code * @author yiminghe@gmail.com * @ignore */ var util = require('./util'); var xtplAstToJs; // codeTemplates --------------------------- start var TOP_DECLARATION = ['var tpl = this,', 'nativeCommands = tpl.root.nativeCommands,', 'utils = tpl.root.utils;'].join('\n'); var CALL_NATIVE_COMMAND = '{lhs} = {name}Command.call(tpl, scope, {option}, buffer, {lineNumber});'; var CALL_CUSTOM_COMMAND = 'buffer = callCommandUtil(tpl, scope, {option}, buffer, [{idParts}], {lineNumber});'; var CALL_FUNCTION = '{lhs} = callFnUtil(tpl, scope, {option}, buffer, [{idParts}], {depth},{lineNumber});'; var SCOPE_RESOLVE = 'var {lhs} = scope.resolve([{idParts}],{depth});'; var REQUIRE_MODULE = 're' + 'quire("{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 = '{name}Command = nativeCommands["{name}"]'; var DECLARE_UTILS = '{name}Util = utils["{name}"]'; var BUFFER_WRITE = 'buffer.write({value},{escape});'; 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 = 'var ' + nativeCode.join(',\n') + ';'; var doubleReg = /\\*"/g, singleReg = /\\*'/g, arrayPush = [].push, uuid = 0; function isGlobalId(node) { if (globals[node.string]) { return 1; } return 0; } function guid(str) { return str + (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) { var source = [], type = e.opType, exp1, exp2, code1Source, code2Source, code1 = xtplAstToJs[e.op1.type](e.op1), code2 = xtplAstToJs[e.op2.type](e.op2); exp1 = code1.exp; exp2 = code2.exp; var exp = guid('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 }; } // consider x[d] function getIdStringFromIdParts(source, idParts) { if (idParts.length === 1) { return null; } var i, l, idPart, idPartType, check = 0, nextIdNameCode; for (i = 0, l = idParts.length; i < l; i++) { if (idParts[i].type) { check = 1; break; } } if (check) { var ret = []; for (i = 0, l = idParts.length; i < l; i++) { idPart = idParts[i]; idPartType = idPart.type; if (idPartType) { nextIdNameCode = xtplAstToJs[idPartType](idPart); pushToArray(source, nextIdNameCode.source); ret.push(nextIdNameCode.exp); } else { // literal a.x ret.push(wrapByDoubleQuote(idPart)); } } return ret; } else { return null; } } function genFunction(statements) { var source = ['function(scope, buffer) {']; for (var i = 0, len = statements.length; i < len; i++) { pushToArray(source, xtplAstToJs[statements[i].type](statements[i]).source); } source.push(RETURN_BUFFER); source.push('}'); return source; } function genTopFunction(xtplAstToJs, statements) { var source = [ TOP_DECLARATION, nativeCode ]; for (var i = 0, len = statements.length; i < len; i++) { pushToArray(source, xtplAstToJs[statements[i].type](statements[i]).source); } source.push(RETURN_BUFFER); return { params: ['scope', 'buffer', 'undefined'], source: source.join('\n') }; } function genOptionFromFunction(func, escape) { var optionName = guid('option'); var source = ['var ' + optionName + ' = {' + (escape ? 'escape: 1' : '') + '};'], params = func.params, hash = func.hash; if (params) { var paramsName = guid('params'); source.push('var ' + paramsName + ' = [];'); each(params, function (param) { var nextIdNameCode = xtplAstToJs[param.type](param); pushToArray(source, nextIdNameCode.source); source.push(paramsName + '.push(' + nextIdNameCode.exp + ');'); }); source.push(optionName + '.params = ' + paramsName + ';'); } if (hash) { var hashName = guid('hash'); source.push('var ' + hashName + ' = {};'); each(hash.value, function (v, key) { var nextIdNameCode = xtplAstToJs[v.type](v); pushToArray(source, nextIdNameCode.source); source.push(hashName + '[' + wrapByDoubleQuote(key) + '] = ' + nextIdNameCode.exp + ';'); }); source.push(optionName + '.hash = ' + hashName + ';'); } return { exp: optionName, source: source }; } function generateFunction(xtplAstToJs, func, escape, block) { var source = []; var functionConfigCode, optionName, idName; var id = func.id; var idString = id.string; var idParts = id.parts; var lineNumber = id.lineNumber; var i; if (idString === 'elseif') { return { exp: '', source: [] }; } functionConfigCode = genOptionFromFunction(func, escape); 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(thenStatements).join('\n') + ';'); if (inverse) { source.push(optionName + '.inverse = ' + genFunction(inverse).join('\n') + ';'); } if (elseIfs.length) { var elseIfsVariable = guid('elseIfs'); source.push('var ' + elseIfsVariable + ' = []'); for (i = 0; i < elseIfs.length; i++) { var elseIfStatement = elseIfs[i]; var elseIfVariable = guid('elseIf'); source.push('var ' + elseIfVariable + ' = {}'); var condition = elseIfStatement.condition; var conditionCode = xtplAstToJs[condition.type](condition); source.push(elseIfVariable + '.test = function(scope){'); pushToArray(source, conditionCode.source); source.push('return (' + conditionCode.exp + ');'); source.push('};'); source.push(elseIfVariable + '.fn = ' + genFunction(elseIfStatement.statements).join('\n') + ';'); source.push(elseIfsVariable + '.push(' + elseIfVariable + ');'); } source.push(optionName + '.elseIfs = ' + elseIfsVariable + ';'); } } if (xtplAstToJs.isModule) { // require include/extend modules if (idString === 'include' || idString === 'extend') { // prevent require parse... source.push(substitute(REQUIRE_MODULE, { name: func.params[0].value })); } } if (!block) { idName = guid('callRet'); source.push('var ' + idName); } if (idString in nativeCommands) { source.push(substitute(CALL_NATIVE_COMMAND, { lhs: block ? 'buffer' : idName, name: idString, option: optionName, lineNumber: lineNumber })); } else if (block) { source.push(substitute(CALL_CUSTOM_COMMAND, { option: optionName, idParts: joinArrayOfString(idParts), lineNumber: lineNumber })); } else { // x.y(1,2) // {x:{y:function(a,b){}}} var newParts = getIdStringFromIdParts(source, idParts); source.push(substitute(CALL_FUNCTION, { lhs: idName, option: optionName, idParts: (newParts ? newParts.join(',') : joinArrayOfString(idParts)), depth: id.depth, lineNumber: lineNumber })); } if (idName) { source.push(substitute(CHECK_BUFFER, { name: idName })); } return { exp: idName, source: source }; } xtplAstToJs = { arrayExpression: function (e) { var list = e.list; var len = list.length; var r; var source = []; var exp = []; for (var i = 0; i < len; i++) { r = xtplAstToJs[list[i].type](list[i]); source.push.apply(source, r.source); exp.push(r.exp); } return { exp: '[' + exp.join(',') + ']', source: source }; }, jsonExpression: function (e) { 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 = xtplAstToJs[item[1].type](item[1]); source.push.apply(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) { var code = xtplAstToJs[e.value.type](e.value); 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) { if (isGlobalId(idNode)) { return { exp: idNode.string, source: [] }; } var source = [], depth = idNode.depth, idParts = idNode.parts, idName = guid('id'); // variable {{variable[subVariable]}} var newParts = getIdStringFromIdParts(source, idParts); // optimize for x.y.z source.push(substitute(SCOPE_RESOLVE, { lhs: idName, idParts: newParts ? newParts.join(',') : joinArrayOfString(idParts), depth: depth })); return { exp: idName, source: source }; }, 'function': function (func, escape) { return generateFunction(this, func, escape); }, blockStatement: function (block) { return generateFunction(this, block.func, block.escape, block); }, expressionStatement: function (expressionStatement) { var source = [], escape = expressionStatement.escape, code, expression = expressionStatement.value, type = expression.type, expressionOrVariable; code = xtplAstToJs[type](expression, escape); pushToArray(source, code.source); expressionOrVariable = code.exp; source.push(substitute(BUFFER_WRITE, { value: expressionOrVariable, escape: !!escape })); return { exp: '', source: source }; }, contentStatement: function (contentStatement) { /*jshint quotmark:false*/ return { exp: '', source: [ substitute(BUFFER_WRITE, { value: wrapBySingleQuote(escapeString(contentStatement.value, 0)), escape: 0 }) ] }; } }; var compiler; /** * compiler for xtemplate * @class KISSY.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 root = compiler.parse(param.content, param.name); uuid = 0; xtplAstToJs.isModule = param.isModule; return genTopFunction(xtplAstToJs, root.statements); }, /** * 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 || guid('xtemplate') }); // 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().xtplAstToJs() */