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 */
27var VAR_index = '$index';
28var VAR_count = '$count';
29var VAR_this = '$this';
30var VAR_context = '$context';
31var 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 */
39var 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 */
47var CHAR_colon = ':';
48var REGEXP_semicolon = /\s*;\s*/;
49
50
51/**
52 * See constructor_()
53 * @param {Object|null=} opt_data
54 * @param {Object=} opt_parent
55 * @constructor
56 */
57function 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 * @private
73 */
74JsEvalContext.prototype.constructor_ = function(opt_data, opt_parent) {
75  var me = this;
76
77  if (!me.vars_) {
78    /**
79     * The context for variable definitions in which the jstemplate
80     * expressions are evaluated. Other than for the local context,
81     * which replaces the parent context, variable definitions of the
82     * parent are inherited. The special variable $this points to data_.
83     *
84     * If this instance is recycled from the cache, then the property is
85     * already initialized.
86     *
87     * @type {Object}
88     */
89    me.vars_ = {};
90  }
91  if (opt_parent) {
92    // If there is a parent node, inherit local variables from the
93    // parent.
94    copyProperties(me.vars_, opt_parent.vars_);
95  } else {
96    // If a root node, inherit global symbols. Since every parent
97    // chain has a root with no parent, global variables will be
98    // present in the case above too. This means that globals can be
99    // overridden by locals, as it should be.
100    copyProperties(me.vars_, JsEvalContext.globals_);
101  }
102
103  /**
104   * The current context object is assigned to the special variable
105   * $this so it is possible to use it in expressions.
106   * @type {Object}
107   */
108  me.vars_[VAR_this] = opt_data;
109
110  /**
111   * The entire context structure is exposed as a variable so it can be
112   * passed to javascript invocations through jseval.
113   */
114  me.vars_[VAR_context] = me;
115
116  /**
117   * The local context of the input data in which the jstemplate
118   * expressions are evaluated. Notice that this is usually an Object,
119   * but it can also be a scalar value (and then still the expression
120   * $this can be used to refer to it). Notice this can even be value,
121   * undefined or null. Hence, we have to protect jsexec() from using
122   * undefined or null, yet we want $this to reflect the true value of
123   * the current context. Thus we assign the original value to $this,
124   * above, but for the expression context we replace null and
125   * undefined by the empty string.
126   *
127   * @type {*}
128   */
129  me.data_ = getDefaultObject(opt_data, STRING_empty);
130
131  if (!opt_parent) {
132    // If this is a top-level context, create a variable reference to the data
133    // to allow for  accessing top-level properties of the original context
134    // data from child contexts.
135    me.vars_[VAR_top] = me.data_;
136  }
137};
138
139
140/**
141 * A map of globally defined symbols. Every instance of JsExprContext
142 * inherits them in its vars_.
143 * @type Object
144 */
145JsEvalContext.globals_ = {}
146
147
148/**
149 * Sets a global symbol. It will be available like a variable in every
150 * JsEvalContext instance. This is intended mainly to register
151 * immutable global objects, such as functions, at load time, and not
152 * to add global data at runtime. I.e. the same objections as to
153 * global variables in general apply also here. (Hence the name
154 * "global", and not "global var".)
155 * @param {string} name
156 * @param {Object|null} value
157 */
158JsEvalContext.setGlobal = function(name, value) {
159  JsEvalContext.globals_[name] = value;
160};
161
162
163/**
164 * Set the default value to be returned if context evaluation results in an
165 * error. (This can occur if a non-existent value was requested).
166 */
167JsEvalContext.setGlobal(GLOB_default, null);
168
169
170/**
171 * A cache to reuse JsEvalContext instances. (IE6 perf)
172 *
173 * @type Array.<JsEvalContext>
174 */
175JsEvalContext.recycledInstances_ = [];
176
177
178/**
179 * A factory to create a JsEvalContext instance, possibly reusing
180 * one from recycledInstances_. (IE6 perf)
181 *
182 * @param {Object} opt_data
183 * @param {JsEvalContext} opt_parent
184 * @return {JsEvalContext}
185 */
186JsEvalContext.create = function(opt_data, opt_parent) {
187  if (jsLength(JsEvalContext.recycledInstances_) > 0) {
188    var instance = JsEvalContext.recycledInstances_.pop();
189    JsEvalContext.call(instance, opt_data, opt_parent);
190    return instance;
191  } else {
192    return new JsEvalContext(opt_data, opt_parent);
193  }
194};
195
196
197/**
198 * Recycle a used JsEvalContext instance, so we can avoid creating one
199 * the next time we need one. (IE6 perf)
200 *
201 * @param {JsEvalContext} instance
202 */
203JsEvalContext.recycle = function(instance) {
204  for (var i in instance.vars_) {
205    // NOTE(mesch): We avoid object creation here. (IE6 perf)
206    delete instance.vars_[i];
207  }
208  instance.data_ = null;
209  JsEvalContext.recycledInstances_.push(instance);
210};
211
212
213/**
214 * Executes a function created using jsEvalToFunction() in the context
215 * of vars, data, and template.
216 *
217 * @param {Function} exprFunction A javascript function created from
218 * a jstemplate attribute value.
219 *
220 * @param {Element} template DOM node of the template.
221 *
222 * @return {Object|null} The value of the expression from which
223 * exprFunction was created in the current js expression context and
224 * the context of template.
225 */
226JsEvalContext.prototype.jsexec = function(exprFunction, template) {
227  try {
228    return exprFunction.call(template, this.vars_, this.data_);
229  } catch (e) {
230    log('jsexec EXCEPTION: ' + e + ' at ' + template +
231        ' with ' + exprFunction);
232    return JsEvalContext.globals_[GLOB_default];
233  }
234};
235
236
237/**
238 * Clones the current context for a new context object. The cloned
239 * context has the data object as its context object and the current
240 * context as its parent context. It also sets the $index variable to
241 * the given value. This value usually is the position of the data
242 * object in a list for which a template is instantiated multiply.
243 *
244 * @param {Object} data The new context object.
245 *
246 * @param {number} index Position of the new context when multiply
247 * instantiated. (See implementation of jstSelect().)
248 *
249 * @param {number} count The total number of contexts that were multiply
250 * instantiated. (See implementation of jstSelect().)
251 *
252 * @return {JsEvalContext}
253 */
254JsEvalContext.prototype.clone = function(data, index, count) {
255  var ret = JsEvalContext.create(data, this);
256  ret.setVariable(VAR_index, index);
257  ret.setVariable(VAR_count, count);
258  return ret;
259};
260
261
262/**
263 * Binds a local variable to the given value. If set from jstemplate
264 * jsvalue expressions, variable names must start with $, but in the
265 * API they only have to be valid javascript identifier.
266 *
267 * @param {string} name
268 * @param {*} value
269 */
270JsEvalContext.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 {*} value
284 */
285JsEvalContext.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 */
299JsEvalContext.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 */
308var STRING_a = 'a_';
309var STRING_b = 'b_';
310var STRING_with = 'with (a_) with (b_) return ';
311
312
313/**
314 * Cache for jsEvalToFunction results.
315 * @type Object
316 */
317JsEvalContext.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 */
331function 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 */
353function 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 */
369function 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 */
399function 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}
410