OLD | NEW |
| (Empty) |
1 // Copyright 2006 Google Inc. | |
2 // | |
3 // Licensed under the Apache License, Version 2.0 (the "License"); | |
4 // you may not use this file except in compliance with the License. | |
5 // You may obtain a copy of the License at | |
6 // | |
7 // http://www.apache.org/licenses/LICENSE-2.0 | |
8 // | |
9 // Unless required by applicable law or agreed to in writing, software | |
10 // distributed under the License is distributed on an "AS IS" BASIS, | |
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
12 // implied. See the License for the specific language governing | |
13 // permissions and limitations under the License. | |
14 /** | |
15 * Author: Steffen Meschkat <mesch@google.com> | |
16 * | |
17 * @fileoverview This class is used to evaluate expressions in a local | |
18 * context. Used by JstProcessor. | |
19 */ | |
20 | |
21 | |
22 /** | |
23 * Names of special variables defined by the jstemplate evaluation | |
24 * context. These can be used in js expression in jstemplate | |
25 * attributes. | |
26 */ | |
27 var VAR_index = '$index'; | |
28 var VAR_count = '$count'; | |
29 var VAR_this = '$this'; | |
30 var VAR_context = '$context'; | |
31 var VAR_top = '$top'; | |
32 | |
33 | |
34 /** | |
35 * The name of the global variable which holds the value to be returned if | |
36 * context evaluation results in an error. | |
37 * Use JsEvalContext.setGlobal(GLOB_default, value) to set this. | |
38 */ | |
39 var GLOB_default = '$default'; | |
40 | |
41 | |
42 /** | |
43 * Un-inlined literals, to avoid object creation in IE6. TODO(mesch): | |
44 * So far, these are only used here, but we could use them thoughout | |
45 * the code and thus move them to constants.js. | |
46 */ | |
47 var CHAR_colon = ':'; | |
48 var REGEXP_semicolon = /\s*;\s*/; | |
49 | |
50 | |
51 /** | |
52 * See constructor_() | |
53 * @param {Object|null} opt_data | |
54 * @param {Object} opt_parent | |
55 * @constructor | |
56 */ | |
57 function JsEvalContext(opt_data, opt_parent) { | |
58 this.constructor_.apply(this, arguments); | |
59 } | |
60 | |
61 /** | |
62 * Context for processing a jstemplate. The context contains a context | |
63 * object, whose properties can be referred to in jstemplate | |
64 * expressions, and it holds the locally defined variables. | |
65 * | |
66 * @param {Object|null} opt_data The context object. Null if no context. | |
67 * | |
68 * @param {Object} opt_parent The parent context, from which local | |
69 * variables are inherited. Normally the context object of the parent | |
70 * context is the object whose property the parent object is. Null for the | |
71 * context of the root object. | |
72 */ | |
73 JsEvalContext.prototype.constructor_ = function(opt_data, opt_parent) { | |
74 var me = this; | |
75 | |
76 /** | |
77 * The context for variable definitions in which the jstemplate | |
78 * expressions are evaluated. Other than for the local context, | |
79 * which replaces the parent context, variable definitions of the | |
80 * parent are inherited. The special variable $this points to data_. | |
81 * | |
82 * If this instance is recycled from the cache, then the property is | |
83 * already initialized. | |
84 * | |
85 * @type {Object} | |
86 */ | |
87 if (!me.vars_) { | |
88 me.vars_ = {}; | |
89 } | |
90 if (opt_parent) { | |
91 // If there is a parent node, inherit local variables from the | |
92 // parent. | |
93 copyProperties(me.vars_, opt_parent.vars_); | |
94 } else { | |
95 // If a root node, inherit global symbols. Since every parent | |
96 // chain has a root with no parent, global variables will be | |
97 // present in the case above too. This means that globals can be | |
98 // overridden by locals, as it should be. | |
99 copyProperties(me.vars_, JsEvalContext.globals_); | |
100 } | |
101 | |
102 /** | |
103 * The current context object is assigned to the special variable | |
104 * $this so it is possible to use it in expressions. | |
105 * @type Object | |
106 */ | |
107 me.vars_[VAR_this] = opt_data; | |
108 | |
109 /** | |
110 * The entire context structure is exposed as a variable so it can be | |
111 * passed to javascript invocations through jseval. | |
112 */ | |
113 me.vars_[VAR_context] = me; | |
114 | |
115 /** | |
116 * The local context of the input data in which the jstemplate | |
117 * expressions are evaluated. Notice that this is usually an Object, | |
118 * but it can also be a scalar value (and then still the expression | |
119 * $this can be used to refer to it). Notice this can even be value, | |
120 * undefined or null. Hence, we have to protect jsexec() from using | |
121 * undefined or null, yet we want $this to reflect the true value of | |
122 * the current context. Thus we assign the original value to $this, | |
123 * above, but for the expression context we replace null and | |
124 * undefined by the empty string. | |
125 * | |
126 * @type {Object|null} | |
127 */ | |
128 me.data_ = getDefaultObject(opt_data, STRING_empty); | |
129 | |
130 if (!opt_parent) { | |
131 // If this is a top-level context, create a variable reference to the data | |
132 // to allow for accessing top-level properties of the original context | |
133 // data from child contexts. | |
134 me.vars_[VAR_top] = me.data_; | |
135 } | |
136 }; | |
137 | |
138 | |
139 /** | |
140 * A map of globally defined symbols. Every instance of JsExprContext | |
141 * inherits them in its vars_. | |
142 * @type Object | |
143 */ | |
144 JsEvalContext.globals_ = {} | |
145 | |
146 | |
147 /** | |
148 * Sets a global symbol. It will be available like a variable in every | |
149 * JsEvalContext instance. This is intended mainly to register | |
150 * immutable global objects, such as functions, at load time, and not | |
151 * to add global data at runtime. I.e. the same objections as to | |
152 * global variables in general apply also here. (Hence the name | |
153 * "global", and not "global var".) | |
154 * @param {string} name | |
155 * @param {Object|null} value | |
156 */ | |
157 JsEvalContext.setGlobal = function(name, value) { | |
158 JsEvalContext.globals_[name] = value; | |
159 }; | |
160 | |
161 | |
162 /** | |
163 * Set the default value to be returned if context evaluation results in an | |
164 * error. (This can occur if a non-existent value was requested). | |
165 */ | |
166 JsEvalContext.setGlobal(GLOB_default, null); | |
167 | |
168 | |
169 /** | |
170 * A cache to reuse JsEvalContext instances. (IE6 perf) | |
171 * | |
172 * @type Array.<JsEvalContext> | |
173 */ | |
174 JsEvalContext.recycledInstances_ = []; | |
175 | |
176 | |
177 /** | |
178 * A factory to create a JsEvalContext instance, possibly reusing | |
179 * one from recycledInstances_. (IE6 perf) | |
180 * | |
181 * @param {Object} opt_data | |
182 * @param {JsEvalContext} opt_parent | |
183 * @return {JsEvalContext} | |
184 */ | |
185 JsEvalContext.create = function(opt_data, opt_parent) { | |
186 if (jsLength(JsEvalContext.recycledInstances_) > 0) { | |
187 var instance = JsEvalContext.recycledInstances_.pop(); | |
188 JsEvalContext.call(instance, opt_data, opt_parent); | |
189 return instance; | |
190 } else { | |
191 return new JsEvalContext(opt_data, opt_parent); | |
192 } | |
193 }; | |
194 | |
195 | |
196 /** | |
197 * Recycle a used JsEvalContext instance, so we can avoid creating one | |
198 * the next time we need one. (IE6 perf) | |
199 * | |
200 * @param {JsEvalContext} instance | |
201 */ | |
202 JsEvalContext.recycle = function(instance) { | |
203 for (var i in instance.vars_) { | |
204 // NOTE(mesch): We avoid object creation here. (IE6 perf) | |
205 delete instance.vars_[i]; | |
206 } | |
207 instance.data_ = null; | |
208 JsEvalContext.recycledInstances_.push(instance); | |
209 }; | |
210 | |
211 | |
212 /** | |
213 * Executes a function created using jsEvalToFunction() in the context | |
214 * of vars, data, and template. | |
215 * | |
216 * @param {Function} exprFunction A javascript function created from | |
217 * a jstemplate attribute value. | |
218 * | |
219 * @param {Element} template DOM node of the template. | |
220 * | |
221 * @return {Object|null} The value of the expression from which | |
222 * exprFunction was created in the current js expression context and | |
223 * the context of template. | |
224 */ | |
225 JsEvalContext.prototype.jsexec = function(exprFunction, template) { | |
226 try { | |
227 return exprFunction.call(template, this.vars_, this.data_); | |
228 } catch (e) { | |
229 log('jsexec EXCEPTION: ' + e + ' at ' + template + | |
230 ' with ' + exprFunction); | |
231 return JsEvalContext.globals_[GLOB_default]; | |
232 } | |
233 }; | |
234 | |
235 | |
236 /** | |
237 * Clones the current context for a new context object. The cloned | |
238 * context has the data object as its context object and the current | |
239 * context as its parent context. It also sets the $index variable to | |
240 * the given value. This value usually is the position of the data | |
241 * object in a list for which a template is instantiated multiply. | |
242 * | |
243 * @param {Object} data The new context object. | |
244 * | |
245 * @param {number} index Position of the new context when multiply | |
246 * instantiated. (See implementation of jstSelect().) | |
247 * | |
248 * @param {number} count The total number of contexts that were multiply | |
249 * instantiated. (See implementation of jstSelect().) | |
250 * | |
251 * @return {JsEvalContext} | |
252 */ | |
253 JsEvalContext.prototype.clone = function(data, index, count) { | |
254 var ret = JsEvalContext.create(data, this); | |
255 ret.setVariable(VAR_index, index); | |
256 ret.setVariable(VAR_count, count); | |
257 return ret; | |
258 }; | |
259 | |
260 | |
261 /** | |
262 * Binds a local variable to the given value. If set from jstemplate | |
263 * jsvalue expressions, variable names must start with $, but in the | |
264 * API they only have to be valid javascript identifier. | |
265 * | |
266 * @param {string} name | |
267 * | |
268 * @param {Object?} value | |
269 */ | |
270 JsEvalContext.prototype.setVariable = function(name, value) { | |
271 this.vars_[name] = value; | |
272 }; | |
273 | |
274 | |
275 /** | |
276 * Returns the value bound to the local variable of the given name, or | |
277 * undefined if it wasn't set. There is no way to distinguish a | |
278 * variable that wasn't set from a variable that was set to | |
279 * undefined. Used mostly for testing. | |
280 * | |
281 * @param {string} name | |
282 * | |
283 * @return {Object?} value | |
284 */ | |
285 JsEvalContext.prototype.getVariable = function(name) { | |
286 return this.vars_[name]; | |
287 }; | |
288 | |
289 | |
290 /** | |
291 * Evaluates a string expression within the scope of this context | |
292 * and returns the result. | |
293 * | |
294 * @param {string} expr A javascript expression | |
295 * @param {Element} opt_template An optional node to serve as "this" | |
296 * | |
297 * @return {Object?} value | |
298 */ | |
299 JsEvalContext.prototype.evalExpression = function(expr, opt_template) { | |
300 var exprFunction = jsEvalToFunction(expr); | |
301 return this.jsexec(exprFunction, opt_template); | |
302 }; | |
303 | |
304 | |
305 /** | |
306 * Uninlined string literals for jsEvalToFunction() (IE6 perf). | |
307 */ | |
308 var STRING_a = 'a_'; | |
309 var STRING_b = 'b_'; | |
310 var STRING_with = 'with (a_) with (b_) return '; | |
311 | |
312 | |
313 /** | |
314 * Cache for jsEvalToFunction results. | |
315 * @type Object | |
316 */ | |
317 JsEvalContext.evalToFunctionCache_ = {}; | |
318 | |
319 | |
320 /** | |
321 * Evaluates the given expression as the body of a function that takes | |
322 * vars and data as arguments. Since the resulting function depends | |
323 * only on expr, we cache the result so we save some Function | |
324 * invocations, and some object creations in IE6. | |
325 * | |
326 * @param {string} expr A javascript expression. | |
327 * | |
328 * @return {Function} A function that returns the value of expr in the | |
329 * context of vars and data. | |
330 */ | |
331 function jsEvalToFunction(expr) { | |
332 if (!JsEvalContext.evalToFunctionCache_[expr]) { | |
333 try { | |
334 // NOTE(mesch): The Function constructor is faster than eval(). | |
335 JsEvalContext.evalToFunctionCache_[expr] = | |
336 new Function(STRING_a, STRING_b, STRING_with + expr); | |
337 } catch (e) { | |
338 log('jsEvalToFunction (' + expr + ') EXCEPTION ' + e); | |
339 } | |
340 } | |
341 return JsEvalContext.evalToFunctionCache_[expr]; | |
342 } | |
343 | |
344 | |
345 /** | |
346 * Evaluates the given expression to itself. This is meant to pass | |
347 * through string attribute values. | |
348 * | |
349 * @param {string} expr | |
350 * | |
351 * @return {string} | |
352 */ | |
353 function jsEvalToSelf(expr) { | |
354 return expr; | |
355 } | |
356 | |
357 | |
358 /** | |
359 * Parses the value of the jsvalues attribute in jstemplates: splits | |
360 * it up into a map of labels and expressions, and creates functions | |
361 * from the expressions that are suitable for execution by | |
362 * JsEvalContext.jsexec(). All that is returned as a flattened array | |
363 * of pairs of a String and a Function. | |
364 * | |
365 * @param {string} expr | |
366 * | |
367 * @return {Array} | |
368 */ | |
369 function jsEvalToValues(expr) { | |
370 // TODO(mesch): It is insufficient to split the values by simply | |
371 // finding semi-colons, as the semi-colon may be part of a string | |
372 // constant or escaped. | |
373 var ret = []; | |
374 var values = expr.split(REGEXP_semicolon); | |
375 for (var i = 0, I = jsLength(values); i < I; ++i) { | |
376 var colon = values[i].indexOf(CHAR_colon); | |
377 if (colon < 0) { | |
378 continue; | |
379 } | |
380 var label = stringTrim(values[i].substr(0, colon)); | |
381 var value = jsEvalToFunction(values[i].substr(colon + 1)); | |
382 ret.push(label, value); | |
383 } | |
384 return ret; | |
385 } | |
386 | |
387 | |
388 /** | |
389 * Parses the value of the jseval attribute of jstemplates: splits it | |
390 * up into a list of expressions, and creates functions from the | |
391 * expressions that are suitable for execution by | |
392 * JsEvalContext.jsexec(). All that is returned as an Array of | |
393 * Function. | |
394 * | |
395 * @param {string} expr | |
396 * | |
397 * @return {Array.<Function>} | |
398 */ | |
399 function jsEvalToExpressions(expr) { | |
400 var ret = []; | |
401 var values = expr.split(REGEXP_semicolon); | |
402 for (var i = 0, I = jsLength(values); i < I; ++i) { | |
403 if (values[i]) { | |
404 var value = jsEvalToFunction(values[i]); | |
405 ret.push(value); | |
406 } | |
407 } | |
408 return ret; | |
409 } | |
OLD | NEW |