/** * This class parses bind template format expressions. * @private */Ext.define('Ext.app.bind.Parser', { extend: 'Ext.parse.Parser', requires: [ 'Ext.util.Format' ], infix: { ':': { priority: 70, // bind tighter than multiplication //<debug> dump: function () { var me = this, ret = { at: me.at, arity: me.arity, value: me.value, operand: me.operand.dump(), fmt: [] }, fmt = me.fmt, i; for (i = 0; i < fmt.length; ++i) { ret.fmt.push(fmt[i].dump()); } return ret; }, //</debug> led: function (left) { // We parse a sequence of ":" separated formatter expressions (like a // traditional "," operator) and gather the sequence in our "fmt" array var me = this; me.arity = 'formatter'; me.operand = left; me.fmt = me.parser.parseFmt(); return me; } }, '?': { priority: 20, led: function(left){ var me = this, parser = me.parser, symbol = parser.symbols[':'], temp; me.condition = left; // temporarily set priority of `:` symbol to 0 temp = symbol.priority; symbol.priority = 0; me.tv = parser.parseExpression(0); me.parser.advance(':'); // restore priority of `:` symbol.priority = temp; me.fv = parser.parseExpression(0); me.arity = 'ternary'; return me; } }, '===': 40, '!==': 40, '==': 40, '!=': 40, '<': 40, '<=': 40, '>': 40, '>=': 40 }, symbols: { '(': { nud: function () { // Handles parenthesized expressions var parser = this.parser, symbol = parser.symbols[':'], ret, temp; // temporarily set priority of `:` symbol to 70 to correctly extract formatters inside parans temp = symbol.priority; symbol.priority = 70; ret = parser.parseExpression(); parser.advance(")"); // restore priority of `:` symbol.priority = temp; return ret; } } }, prefix: { '@': 0 }, tokenizer: { operators: { '@': 'at', '?': 'qmark', '===': 'feq', '!==': 'fneq', '==': 'eq', '!=': 'neq', '<': 'lt', '<=': 'lte', '>': 'gt', '>=': 'gte', '&&': 'and', '||': 'or' } }, /** * Parses the expression from the current position and compiles it as a function. The expression tokens are * stored in the provided arguments. * * Called by Ext.app.bind.Template. * * @param {Array} tokens * @param {Object} tokensMaps * @return {Function} */ compileExpression: function (tokens, tokensMaps) { var me = this, debug, fn; me.tokens = tokens; me.tokensMap = tokensMaps; //<debug> debug = me.token.value === '@' && me.tokenizer.peek(); if (debug) { debug = debug.value === 'debugger'; if (debug) { me.advance(); me.advance(); } } //</debug> fn = me.parseSlot(me.parseExpression(), debug); me.tokens = me.tokensMap = null; return fn; }, /** * Parses the chained format functions and compiles them as a function. * * Called by the grid column formatter. * * @return {Function} */ compileFormat: function(){ var fn; //<debug> try { //</debug> fn = this.parseSlot({ arity: 'formatter', fmt: this.parseFmt(), operand: { arity: 'ident', value: 'dummy' } }); this.expect('(end)'); //<debug> } catch (e) { Ext.raise('Invalid format expression: "' + this.tokenizer.text + '"'); } //</debug> return fn; }, privates: { // Chrome really likes "new Function" to realize the code block (as in it is // 2x-3x faster to call it than using eval), but Firefox chokes on it badly. // IE and Opera are also fine with the "new Function" technique. useEval: Ext.isGecko, escapeRe: /("|'|\\)/g, /** * Parses a series of ":" delimited format expressions. * @return {Ext.parse.Symbol[]} * @private */ parseFmt: function () { // We parse a sequence of ":" separated formatter expressions (like a // traditional "," operator) var me = this, fmt = [], priority = me.symbols[':'].priority, expr; do { if (fmt.length) { me.advance(); } expr = me.parseExpression(priority); if (expr.isIdent || expr.isInvoke) { fmt.push(expr); } else { me.syntaxError(expr.at, 'Expected formatter name'); } } while (me.token.id === ':'); return fmt; }, /** * Parses the expression tree and compiles it as a function * * @param expr * @param {Boolean} debug * @return {Function} * @private */ parseSlot: function (expr, debug) { var me = this, defs = [], body = [], tokens = me.tokens || [], fn, code, i, length, temp; me.definitions = defs; me.body = body; body.push('return ' + me.compile(expr) + ';'); // now we have the tokens length = tokens.length; code = 'var fm = Ext.util.Format,\nme,'; temp = 'var a = Ext.Array.from(values);\nme = scope;\n'; if (tokens.length) { for (i = 0; i < length; i++) { code += 'v' + i + ((i == length - 1) ? ';' : ','); temp += 'v' + i + ' = a[' + i + ']; '; } } else { code += 'v0;'; temp += 'v0 = a[0];'; } defs = Ext.Array.insert(defs, 0, [code]); body = Ext.Array.insert(body, 0, [temp]); body = body.join('\n'); //<debug> if (debug) { body = 'debugger;\n' + body; } //</debug> defs.push( (me.useEval ? '$=' : 'return') + ' function (values, scope) {', body, '}' ); code = defs.join('\n'); fn = me.useEval ? me.evalFn(code) : (new Function('Ext', code))(Ext); me.definitions = me.body = null; return fn; }, /** * Compiles the specified symbol * * @param expr * @return {String} * @private */ compile: function (expr) { var me = this, v; switch (expr.arity) { case 'ident': // identifiers are our expression's tokens return me.addToken(expr.value); case 'literal': v = expr.value; // strings need to be escaped before adding them to formula return (typeof v === 'string') ? '"' + String(v).replace(me.escapeRe, '\\$1') + '"' : v; case 'unary': return me.compileUnary(expr); case 'binary': return me.compileBinary(expr); case 'ternary': return me.compileTernary(expr); case 'formatter': return me.compileFormatter(expr); } return this.syntaxError(expr.at, 'Compile error! Unknown symbol'); }, /** * Compiles unary symbol * * @param expr * @return {String} * @private */ compileUnary: function (expr) { var v = expr.value, op = expr.operand; if (v === '!' || v === '-' || v === '+') { return v + '(' + this.compile(op) + ')'; } else if (v === '@') { // @ should be used to prefix global identifiers and nothing else if(!op.isIdent){ return this.syntaxError(expr.at, 'Compile error! Unexpected symbol'); } return op.value; } return ''; }, /** * Compiles binary symbol * * @param expr * @return {String} * @private */ compileBinary: function (expr) { return '(' + this.compile(expr.lhs) + ' ' + expr.value + ' ' + this.compile(expr.rhs) + ')'; }, /** * Compiles ternary symbol * * @param expr * @return {String} * @private */ compileTernary: function (expr) { return '(' + this.compile(expr.condition) + ' ? ' + this.compile(expr.tv) + ' : ' + this.compile(expr.fv) + ')'; }, /** * Compiles formatter symbol * * @param expr * @return {String} * @private */ compileFormatter: function (expr) { var me = this, fmt = expr.fmt, length = fmt.length, body = [ 'var ret;' ], i; if (fmt.length) { body.push('ret = ' + me.compileFormatFn(fmt[0], me.compile(expr.operand)) + ';'); for (i = 1; i < length; i++) { body.push('ret = ' + me.compileFormatFn(fmt[i], 'ret') + ';'); } } body.push('return ret;'); return me.addFn(body.join('\n')); }, /** * Compiles a single format symbol using `value` as the first argument * * @param expr * @param value * @return {String} * @private */ compileFormatFn: function (expr, value) { var fmt, args = [], code = '', length, i; if (expr.isIdent) { // the function has no arguments fmt = expr.value; } else if (expr.isInvoke) { fmt = expr.operand.value; args = expr.args; } if (fmt.substring(0, 5) === 'this.') { fmt = 'me.' + fmt.substring(5); } else { if (!(fmt in Ext.util.Format)) { return this.syntaxError(expr.at, 'Compile error! Invalid format specified "' + fmt + '"'); } fmt = 'fm.' + fmt; } code += value; length = args.length; for (i = 0; i < length; i++) { code += ', ' + this.compile(args[i]); } return fmt + '(' + code + ')'; }, /** * Adds a new function to the final compiled function * @param body * @return {string} Name of the function * @private */ addFn: function (body) { var defs = this.definitions, name = 'f' + defs.length; defs.push( 'function ' + name + '() {', body, '}' ); return name + '()'; }, /** * Evaluates a function * @param $ * @return {Function} * @private */ evalFn: function ($) { eval($); return $; }, /** * Adds the specified expression token to the internal tokens * @param token * @return {string} Name of the variable assigned for this token in the compiled function * @private */ addToken: function (token) { var tokensMap = this.tokensMap, tokens = this.tokens, pos = 0; // token can be ignored when this function is called via `compileFormatFn` if (tokensMap && tokens) { if (token in tokensMap) { pos = tokensMap[token]; } else { tokensMap[token] = pos = tokens.length; tokens.push(token); } } return 'v' + pos; } } });