/** * 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' ], /** * @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(''); }, /** * 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: { /** * 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 = []), last = 0, length = text.length, pos = 0, i; // Remove the initters so that we don't get called here again. for (i in me._initters) { delete me[i]; } me.tokens = []; me.tokensMap = {}; // text = 'Hello {foo:this.fmt(2,4)} World {bar} - {1}' for (i = 0; i < length;) { last = i; if ((i = text.indexOf('{', last)) < 0) { buffer[pos++] = text.substring(last); break; } if (last < i) { buffer[pos++] = text.substring(last, i); } parser.reset(text, i + 1); slots[pos++] = parser.compileExpression(me.tokens, me.tokensMap); i = parser.token.at + 1; // skip over the "}" token parser.expect('}'); // ensure the next token is "}" } parser.release(); me.single = buffer.length === 0 && slots.length === 1; return me; } }});