/** * This class holds the parsed text for a bind template. The syntax is that of a normal * `Ext.Template` except that substitution tokens can contain dots to reference property * names. * * The template is parsed and stored in a representation like this: * * me.text = 'Hey {foo.bar}! Test {bar} and {foo.bar} with {abc} over {bar:number}' * * me.tokens = [ 'foo.bar', 'bar', 'abc' ] * * me.buffer = [ me.slots = [ * 'Hey ', undefined, * undefined, { token: 'foo.bar', pos: 0 }, * '! Test ', undefined, * undefined, { token: 'bar', pos: 1 }, * ' and ', undefined, * undefined, { token: 'foo.bar', pos: 0 }, * ' with ', undefined, * undefined, { token: 'abc', pos: 2 }, * ' over ', undefined, * undefined { token: 'bar', fmt: 'number', pos: 1 } * ] ] * * @private * @since 5.0.0 */Ext.define('Ext.app.bind.Template', { requires: [ 'Ext.util.Format', 'Ext.app.bind.Parser' ], /** * @cfg {Boolean} escapes * Set to `true` to process escape characters as part of bind expressions. * * The `'\'` character is used to escape the next character, treating it * as a literal character even if it is a `'{'` or other escape. * * The `'~~'` sequence will treat any subsequent characters as a verbatim, * literal expression and no extra processing will take place. This includes * escapes and replacement tokens. * * @since 6.5.2 * @private */ escapes: false, /** * @property {String[]} buffer * Initially this is just the array of string fragments with `null` between each * to hold the place of a substitution token. On first use these slots are filled * with the token's value and this array is joined to form the output. * @private */ buffer: null, /** * @property {Object[]} slots * The elements of this array line up with those of `buffer`. This array holds * the parsed information for the substitution token that fills a given slot in * the generated string. Indices that correspond to literal text are `null`. * * Consider the following substitution token: * * {foo:this.fmt(2,4)} * * The object in this array has the following properties to describe this token: * * * `fmt` The name of the formatting function ("fmt") or `null` if none. * * `index` The numeric index if this is not a named substitution or `null`. * * `not` True if the token has a logical not ("!") at the front. * * `token` The name of the token ("foo") if not an `index`. * * `pos` The position of this token in the `tokens` array. * * `scope` A reference to the object on which the `fmt` method exists. This * will be `Ext.util.Format` if no "this." is present or `null` if it is (or * if there is no `fmt`). In the above example, this is `null` to indicate the * scope is unknown. * * `args` An array of arguments to `fmt` if the arguments are simple enough * to parse directly. Otherwise this is `null` and `fn` is used. * * `fn` A generated function to use to evaluate the arguments to the `fmt`. In * rare cases these arguments can reference global variables so the expression * must be evaluated on each call. * * `format` The method to call to perform the format. This method accepts the * scope (in case `scope` is unknown) and the value. This function is `null` if * there is no `fmt`. * * @private */ slots: null, /** * @property {String[]} tokens * The distinct set of tokens used in the template excluding formatting. This is * used to ensure that only one bind is performed per unique token. This array is * passed to {@link Ext.app.ViewModel#bind} to perform a "multi-bind". The result * is an array of values corresponding these tokens. Each entry in `slots` then * knows its `pos` in this array from which to pick up its value, apply formats * and place in `buffer`. * @private */ tokens: null, /** * @param {String} text The text of the template. */ constructor: function(text) { var me = this, initters = me._initters, name; me.text = text; for (name in initters) { me[name] = initters[name]; } }, /** * @property {Object} _initters * Each of the methods contained on this object are placed in new instances to lazily * parse the template text. * @private * @since 5.0.0 */ _initters: { apply: function(values, scope) { return this.parse().apply(values, scope); }, getTokens: function() { return this.parse().getTokens(); } }, /** * Applies this template to the given `values`. The `values` must correspond to the * `tokens` returned by `getTokens`. * * @param {Array} values The values of the `tokens`. * @param {Object} scope The object instance to use for "this." formatter calls in the * template. * @return {String} * @since 5.0.0 */ apply: function(values, scope) { var me = this, slots = me.slots, buffer = me.buffer, length = slots.length, i, slot; for (i = 0; i < length; ++i) { slot = slots[i]; if (slot) { buffer[i] = slot(values, scope); } } // If we have only one component and it is a slot (a {} component), then we // want to evaluate to whatever that expression generated. if (slot && me.single) { return buffer[0]; } return buffer.join(''); }, getText: function() { return this.buffer.join(''); }, /** * Returns the distinct set of binding tokens for this template. * @return {String[]} The `tokens` for this template. */ getTokens: function() { return this.tokens; }, /** * Returns true if the expression is static, meaning it has no * tokens or slots that need to be evaluated. * * @private */ isStatic: function() { var tokens = this.getTokens(), slots = this.slots; return (tokens.length === 0 && slots.length === 0); }, privates: { literalChar: '~', escapeChar: '\\', /** * Parses the template text into `buffer`, `slots` and `tokens`. This method is called * automatically when the template is first used. * @return {Ext.app.bind.Template} this * @private */ parse: function() { // NOTE: The particulars of what is stored here, while private, are likely to be // important to Sencha Architect so changes need to be coordinated. var me = this, text = me.text, parser = Ext.app.bind.Parser.fly(), buffer = (me.buffer = []), slots = (me.slots = []), length = text.length, pos = 0, escapes = me.escapes, current = '', i = 0, esc = me.escapeChar, lit = me.literalChar, escaped, lastEscaped, c, prev, key; // Remove the initters so that we don't get called here again. for (key in me._initters) { delete me[key]; } me.tokens = []; me.tokensMap = {}; // text = 'Hello {foo:this.fmt(2,4)} World {bar} - {1}' while (i < length) { c = text[i]; lastEscaped = escaped; escaped = escapes && c === esc; if (escaped) { c = text[i + 1]; ++i; } else if (c === lit && prev === lit && !lastEscaped) { current = current.slice(0, -1); current += text.substring(i + 1); break; } else if (c === '{') { if (current) { buffer[pos++] = current; current = ''; } // parse expression parser.reset(text, i + 1); i = me.parseExpression(parser, pos); ++pos; continue; } current += c; ++i; prev = c; } if (current) { buffer[pos] = current; } parser.release(); me.single = buffer.length === 0 && slots.length === 1; return me; }, parseExpression: function(parser, pos) { var i; this.slots[pos] = parser.compileExpression(this.tokens, this.tokensMap); i = parser.token.at + 1; // skip over the "}" token parser.expect('}'); // ensure the next token is "}" return i; } }});