]> git.proxmox.com Git - extjs.git/blame - extjs/packages/core/src/app/bind/Template.js
add extjs 6.0.1 sources
[extjs.git] / extjs / packages / core / src / app / bind / Template.js
CommitLineData
6527f429
DM
1/**\r
2 * This class holds the parsed text for a bind template. The syntax is that of a normal\r
3 * `Ext.Template` except that substitution tokens can contain dots to reference property\r
4 * names.\r
5 *\r
6 * The template is parsed and stored in a representation like this:\r
7 *\r
8 * me.text = 'Hey {foo.bar}! Test {bar} and {foo.bar} with {abc} over {bar:number}'\r
9 *\r
10 * me.tokens = [ 'foo.bar', 'bar', 'abc' ]\r
11 *\r
12 * me.buffer = [ me.slots = [\r
13 * 'Hey ', undefined,\r
14 * undefined, { token: 'foo.bar', pos: 0 },\r
15 * '! Test ', undefined,\r
16 * undefined, { token: 'bar', pos: 1 },\r
17 * ' and ', undefined,\r
18 * undefined, { token: 'foo.bar', pos: 0 },\r
19 * ' with ', undefined,\r
20 * undefined, { token: 'abc', pos: 2 },\r
21 * ' over ', undefined,\r
22 * undefined { token: 'bar', fmt: 'number', pos: 1 }\r
23 * ] ]\r
24 *\r
25 * @private\r
26 * @since 5.0.0\r
27 */\r
28Ext.define('Ext.app.bind.Template', {\r
29 requires: [\r
30 'Ext.util.Format'\r
31 ],\r
32\r
33 numberRe: /^(?:\d+(?:\.\d*)?)$/,\r
34\r
35 stringRe: /^(?:["][^"]*["])$/,\r
36\r
37 /**\r
38 * @property {RegExp} tokenRe\r
39 * Regular expression used to extract tokens.\r
40 *\r
41 * Finds the following expressions within a format string\r
42 *\r
43 * {AND?}\r
44 * / \\r
45 * / \\r
46 * / \\r
47 * / \\r
48 * OR AND?\r
49 * / \ / \\r
50 * / \ / \\r
51 * / \ / \\r
52 * (\d+) ([a-z_][\w\-\.]*) / \\r
53 * index name / \\r
54 * / \\r
55 * / \\r
56 * :([a-z_\.]*) (?:\(([^\)]*?)?\))?\r
57 * formatFn args\r
58 *\r
59 * Numeric index or (name followed by optional formatting function and args)\r
60 * @private\r
61 */\r
62 tokenRe: /\{[!]?(?:(?:(\d+)|([a-z_][\w\-\.]*))(?::([a-z_\.]+)(?:\(([^\)]*?)?\))?)?)\}/gi,\r
63\r
64 formatRe: /^([a-z_]+)(?:\(([^\)]*?)?\))?$/i,\r
65\r
66 /**\r
67 * @property {String[]} buffer\r
68 * Initially this is just the array of string fragments with `null` between each\r
69 * to hold the place of a substitution token. On first use these slots are filled\r
70 * with the token's value and this array is joined to form the output.\r
71 * @private\r
72 */\r
73 buffer: null,\r
74\r
75 /**\r
76 * @property {Object[]} slots\r
77 * The elements of this array line up with those of `buffer`. This array holds\r
78 * the parsed information for the substitution token that fills a given slot in\r
79 * the generated string. Indices that correspond to literal text are `null`.\r
80 *\r
81 * Consider the following substitution token:\r
82 *\r
83 * {foo:this.fmt(2,4)}\r
84 *\r
85 * The object in this array has the following properties to describe this token:\r
86 *\r
87 * * `fmt` The name of the formatting function ("fmt") or `null` if none.\r
88 * * `index` The numeric index if this is not a named substitution or `null`.\r
89 * * `not` True if the token has a logical not ("!") at the front.\r
90 * * `token` The name of the token ("foo") if not an `index`.\r
91 * * `pos` The position of this token in the `tokens` array.\r
92 * * `scope` A reference to the object on which the `fmt` method exists. This\r
93 * will be `Ext.util.Format` if no "this." is present or `null` if it is (or\r
94 * if there is no `fmt`). In the above example, this is `null` to indicate the\r
95 * scope is unknown.\r
96 * * `args` An array of arguments to `fmt` if the arguments are simple enough\r
97 * to parse directly. Otherwise this is `null` and `fn` is used.\r
98 * * `fn` A generated function to use to evaluate the arguments to the `fmt`. In\r
99 * rare cases these arguments can reference global variables so the expression\r
100 * must be evaluated on each call.\r
101 * * `format` The method to call to perform the format. This method accepts the\r
102 * scope (in case `scope` is unknown) and the value. This function is `null` if\r
103 * there is no `fmt`.\r
104 *\r
105 * @private\r
106 */\r
107 slots: null,\r
108\r
109 /**\r
110 * @property {String[]} tokens\r
111 * The distinct set of tokens used in the template excluding formatting. This is\r
112 * used to ensure that only one bind is performed per unique token. This array is\r
113 * passed to {@link Ext.app.ViewModel#bind} to perform a "multi-bind". The result\r
114 * is an array of values corresponding these tokens. Each entry in `slots` then\r
115 * knows its `pos` in this array from which to pick up its value, apply formats\r
116 * and place in `buffer`.\r
117 * @private\r
118 */\r
119 tokens: null,\r
120\r
121 /**\r
122 * @param {String} text The text of the template.\r
123 */\r
124 constructor: function (text) {\r
125 var me = this,\r
126 initters = me._initters,\r
127 name;\r
128\r
129 me.text = text;\r
130\r
131 for (name in initters) {\r
132 me[name] = initters[name];\r
133 }\r
134 },\r
135\r
136 /**\r
137 * @property {Object} _initters\r
138 * Each of the methods contained on this object are placed in new instances to lazily\r
139 * parse the template text.\r
140 * @private\r
141 * @since 5.0.0\r
142 */\r
143 _initters: {\r
144 apply: function (values, scope) {\r
145 return this.parse().apply(values, scope);\r
146 },\r
147 getTokens: function () {\r
148 return this.parse().getTokens();\r
149 }\r
150 },\r
151\r
152 /**\r
153 * Applies this template to the given `values`. The `values` must correspond to the\r
154 * `tokens` returned by `getTokens`.\r
155 *\r
156 * @param {Array} values The values of the `tokens`.\r
157 * @param {Object} scope The object instance to use for "this." formatter calls in the\r
158 * template.\r
159 * @return {String}\r
160 * @since 5.0.0\r
161 */\r
162 apply: function (values, scope) {\r
163 var me = this,\r
164 slots = me.slots,\r
165 buffer = me.buffer,\r
166 length = slots.length, // === buffer.length\r
167 i, slot, value;\r
168\r
169 for (i = 0; i < length; ++i) {\r
170 slot = slots[i];\r
171 if (slot) {\r
172 if ((value = values[slot.pos]) == null) {\r
173 // map (value === null || value === undefined) to '':\r
174 value = '';\r
175 }\r
176 if (slot.not) {\r
177 value = !value;\r
178 }\r
179 if (slot.format) {\r
180 value = slot.format(value, scope);\r
181 }\r
182 buffer[i] = value;\r
183 }\r
184 }\r
185\r
186 return buffer.join('');\r
187 },\r
188\r
189 /**\r
190 * Returns the distinct set of binding tokens for this template.\r
191 * @return {String[]} The `tokens` for this template.\r
192 */\r
193 getTokens: function () {\r
194 return this.tokens;\r
195 },\r
196\r
197 /**\r
198 * Parses the template text into `buffer`, `slots` and `tokens`. This method is called\r
199 * automatically when the template is first used.\r
200 * @return {Ext.app.bind.Template} this\r
201 * @private\r
202 */\r
203 parse: function () {\r
204 // NOTE: The particulars of what is stored here, while private, are likely to be\r
205 // important to Sencha Architect so changes need to be coordinated.\r
206 var me = this,\r
207 text = me.text,\r
208 buffer = [],\r
209 slots = [],\r
210 tokens = [],\r
211 tokenMap = {},\r
212 last = 0,\r
213 tokenRe = me.tokenRe,\r
214 pos = 0,\r
215 fmt, i, length, match, s, slot, token;\r
216\r
217 // Remove the initters so that we don't get called here again.\r
218 for (i in me._initters) {\r
219 delete me[i];\r
220 }\r
221\r
222 me.buffer = buffer;\r
223\r
224 me.slots = slots;\r
225\r
226 me.tokens = tokens;\r
227\r
228 // text = 'Hello {foo:this.fmt(2,4)} World {bar} - {1}'\r
229 while ((match = tokenRe.exec(text))) {\r
230 // 0 1 2 3 4 index\r
231 // [ '{foo:this.fmt(2,4)}', undefined, 'foo', 'this.fmt', '2,4'] 6\r
232 // [ '{bar}', undefined, 'bar', undefined, undefined] 32\r
233 // [ '{1}', '1', undefined, undefined, undefined] 40\r
234 length = match.index - last;\r
235 if (length) {\r
236 buffer[pos++] = text.substring(last, last + length);\r
237 last += length;\r
238 }\r
239 last += (s = match[0]).length;\r
240\r
241 slot = {\r
242 fmt: (fmt = match[3] || null),\r
243 index: match[1] ? parseInt(match[1], 10) : null,\r
244 not: s.charAt(1) === '!',\r
245 token: match[2] || null\r
246 };\r
247\r
248 token = slot.token || String(slot.index);\r
249 if (token in tokenMap) {\r
250 slot.pos = tokenMap[token];\r
251 } else {\r
252 tokenMap[token] = slot.pos = tokens.length;\r
253 tokens.push(token);\r
254 }\r
255\r
256 if (fmt) {\r
257 if (fmt.substring(0,5) === 'this.') {\r
258 slot.fmt = fmt.substring(5);\r
259 } else {\r
260 //<debug>\r
261 if (!(fmt in Ext.util.Format)) {\r
262 Ext.raise('Invalid format specified: "' + fmt + '"');\r
263 }\r
264 //</debug>\r
265 slot.scope = Ext.util.Format;\r
266 }\r
267\r
268 me.parseArgs(match[4], slot);\r
269 }\r
270\r
271 slots[pos++] = slot;\r
272 }\r
273\r
274 if (last < text.length) {\r
275 buffer[pos++] = text.substring(last);\r
276 }\r
277\r
278 return me;\r
279 },\r
280\r
281 parseArgs: function (argsString, slot) {\r
282 var me = this,\r
283 numberRe = me.numberRe,\r
284 stringRe = me.stringRe,\r
285 arg, args, i, length;\r
286\r
287 if (!argsString) {\r
288 args = [];\r
289 } else if (argsString.indexOf(',') < 0) {\r
290 args = [argsString];\r
291 } else {\r
292 args = argsString.split(',');\r
293 }\r
294\r
295 slot = slot || {};\r
296 length = args.length;\r
297 slot.args = args;\r
298\r
299 for (i = 0; i < length; ++i) {\r
300 arg = args[i];\r
301 if (arg === 'true') {\r
302 args[i] = true;\r
303 } else if (arg === 'false') {\r
304 args[i] = false;\r
305 } else if (arg === 'null') {\r
306 args[i] = null;\r
307 } else if (numberRe.test(arg)) {\r
308 args[i] = parseFloat(arg);\r
309 } else if (stringRe.test(arg)) {\r
310 args[i] = arg.substring(1, arg.length - 1);\r
311 } else {\r
312 slot.fn = Ext.functionFactory('return ['+ argsString +'];');\r
313 slot.format = me._formatEval;\r
314 break;\r
315 }\r
316 }\r
317\r
318 if (!slot.format) {\r
319 // make room for the value at index 0\r
320 args.unshift(0);\r
321 slot.format = me._formatArgs;\r
322 }\r
323\r
324 return slot;\r
325 },\r
326\r
327 /**\r
328 * This method parses token formats and returns an object with a `format` method that\r
329 * can format values accordingly.\r
330 * @param {String} fmt The format suffix of a template token. For example, in the\r
331 * token "{foo:round(2)}" the format is "round(2)".\r
332 * @return {Object} An object with a `format` method to format values.\r
333 * @private\r
334 * @since 5.0.0\r
335 */\r
336 parseFormat: function (fmt) {\r
337 var me = this,\r
338 match = me.formatRe.exec(fmt),\r
339 slot = {\r
340 fmt: fmt,\r
341 scope: Ext.util.Format\r
342 },\r
343 args;\r
344\r
345 //<debug>\r
346 if (!match) {\r
347 Ext.raise('Invalid format syntax: "' + slot + '"');\r
348 }\r
349 //</debug>\r
350\r
351 args = match[2];\r
352 if (args) {\r
353 slot.fmt = match[1];\r
354 me.parseArgs(args, slot);\r
355 } else {\r
356 slot.args = [0]; // for the value\r
357 slot.format = me._formatArgs;\r
358 }\r
359\r
360 return slot;\r
361 },\r
362\r
363 /**\r
364 * This method is placed on an entry in `slots` as the `format` method when that entry\r
365 * has `args` that could be parsed from the template.\r
366 * @param {Object} value The value of the token.\r
367 * @param {Object} [scope] The object instance to use for "this." formatter calls in the\r
368 * template.\r
369 * @return {String} The formatted result to place in `buffer`.\r
370 * @private\r
371 * @since 5.0.0\r
372 */\r
373 _formatArgs: function (value, scope) {\r
374 // NOTE: our "this" pointer is the object in the "slots" array!\r
375 scope = this.scope || scope;\r
376 this.args[0] = value; // index 0 is reserved for the value\r
377 return scope[this.fmt].apply(scope, this.args);\r
378 },\r
379\r
380 /**\r
381 * This method is placed on an entry in `slots` as the `format` method when that entry\r
382 * does not have a parsed `args` array.\r
383 * @param {Object} value The value of the token.\r
384 * @param {Object} [scope] The object instance to use for "this." formatter calls in the\r
385 * template.\r
386 * @return {String} The formatted result to place in `buffer`.\r
387 * @private\r
388 * @since 5.0.0\r
389 */\r
390 _formatEval: function (value, scope) {\r
391 // NOTE: our "this" pointer is the object in the "slots" array!\r
392 var args = this.fn(); // invoke to get the args array\r
393 args.unshift(value); // inject the value at the front\r
394 scope = this.scope || scope;\r
395 return scope[this.fmt].apply(scope, args);\r
396 }\r
397});\r