// Copyright 2006 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or // implied. See the License for the specific language governing // permissions and limitations under the License. /** * Author: Steffen Meschkat <mesch@google.com> * * @fileoverview This class is used to evaluate expressions in a local * context. Used by JstProcessor. */ /** * Names of special variables defined by the jstemplate evaluation * context. These can be used in js expression in jstemplate * attributes. */ var VAR_index = '$index'; var VAR_count = '$count'; var VAR_this = '$this'; var VAR_context = '$context'; var VAR_top = '$top'; /** * The name of the global variable which holds the value to be returned if * context evaluation results in an error. * Use JsEvalContext.setGlobal(GLOB_default, value) to set this. */ var GLOB_default = '$default'; /** * Un-inlined literals, to avoid object creation in IE6. TODO(mesch): * So far, these are only used here, but we could use them thoughout * the code and thus move them to constants.js. */ var CHAR_colon = ':'; var REGEXP_semicolon = /\s*;\s*/; /** * See constructor_() * @param {Object|null} opt_data * @param {Object} opt_parent * @constructor */ function JsEvalContext(opt_data, opt_parent) { this.constructor_.apply(this, arguments); } /** * Context for processing a jstemplate. The context contains a context * object, whose properties can be referred to in jstemplate * expressions, and it holds the locally defined variables. * * @param {Object|null} opt_data The context object. Null if no context. * * @param {Object} opt_parent The parent context, from which local * variables are inherited. Normally the context object of the parent * context is the object whose property the parent object is. Null for the * context of the root object. */ JsEvalContext.prototype.constructor_ = function(opt_data, opt_parent) { var me = this; /** * The context for variable definitions in which the jstemplate * expressions are evaluated. Other than for the local context, * which replaces the parent context, variable definitions of the * parent are inherited. The special variable $this points to data_. * * If this instance is recycled from the cache, then the property is * already initialized. * * @type {Object} */ if (!me.vars_) { me.vars_ = {}; } if (opt_parent) { // If there is a parent node, inherit local variables from the // parent. copyProperties(me.vars_, opt_parent.vars_); } else { // If a root node, inherit global symbols. Since every parent // chain has a root with no parent, global variables will be // present in the case above too. This means that globals can be // overridden by locals, as it should be. copyProperties(me.vars_, JsEvalContext.globals_); } /** * The current context object is assigned to the special variable * $this so it is possible to use it in expressions. * @type Object */ me.vars_[VAR_this] = opt_data; /** * The entire context structure is exposed as a variable so it can be * passed to javascript invocations through jseval. */ me.vars_[VAR_context] = me; /** * The local context of the input data in which the jstemplate * expressions are evaluated. Notice that this is usually an Object, * but it can also be a scalar value (and then still the expression * $this can be used to refer to it). Notice this can even be value, * undefined or null. Hence, we have to protect jsexec() from using * undefined or null, yet we want $this to reflect the true value of * the current context. Thus we assign the original value to $this, * above, but for the expression context we replace null and * undefined by the empty string. * * @type {Object|null} */ me.data_ = getDefaultObject(opt_data, STRING_empty); if (!opt_parent) { // If this is a top-level context, create a variable reference to the data // to allow for accessing top-level properties of the original context // data from child contexts. me.vars_[VAR_top] = me.data_; } }; /** * A map of globally defined symbols. Every instance of JsExprContext * inherits them in its vars_. * @type Object */ JsEvalContext.globals_ = {} /** * Sets a global symbol. It will be available like a variable in every * JsEvalContext instance. This is intended mainly to register * immutable global objects, such as functions, at load time, and not * to add global data at runtime. I.e. the same objections as to * global variables in general apply also here. (Hence the name * "global", and not "global var".) * @param {string} name * @param {Object|null} value */ JsEvalContext.setGlobal = function(name, value) { JsEvalContext.globals_[name] = value; }; /** * Set the default value to be returned if context evaluation results in an * error. (This can occur if a non-existent value was requested). */ JsEvalContext.setGlobal(GLOB_default, null); /** * A cache to reuse JsEvalContext instances. (IE6 perf) * * @type Array.<JsEvalContext> */ JsEvalContext.recycledInstances_ = []; /** * A factory to create a JsEvalContext instance, possibly reusing * one from recycledInstances_. (IE6 perf) * * @param {Object} opt_data * @param {JsEvalContext} opt_parent * @return {JsEvalContext} */ JsEvalContext.create = function(opt_data, opt_parent) { if (jsLength(JsEvalContext.recycledInstances_) > 0) { var instance = JsEvalContext.recycledInstances_.pop(); JsEvalContext.call(instance, opt_data, opt_parent); return instance; } else { return new JsEvalContext(opt_data, opt_parent); } }; /** * Recycle a used JsEvalContext instance, so we can avoid creating one * the next time we need one. (IE6 perf) * * @param {JsEvalContext} instance */ JsEvalContext.recycle = function(instance) { for (var i in instance.vars_) { // NOTE(mesch): We avoid object creation here. (IE6 perf) delete instance.vars_[i]; } instance.data_ = null; JsEvalContext.recycledInstances_.push(instance); }; /** * Executes a function created using jsEvalToFunction() in the context * of vars, data, and template. * * @param {Function} exprFunction A javascript function created from * a jstemplate attribute value. * * @param {Element} template DOM node of the template. * * @return {Object|null} The value of the expression from which * exprFunction was created in the current js expression context and * the context of template. */ JsEvalContext.prototype.jsexec = function(exprFunction, template) { try { return exprFunction.call(template, this.vars_, this.data_); } catch (e) { log('jsexec EXCEPTION: ' + e + ' at ' + template + ' with ' + exprFunction); return JsEvalContext.globals_[GLOB_default]; } }; /** * Clones the current context for a new context object. The cloned * context has the data object as its context object and the current * context as its parent context. It also sets the $index variable to * the given value. This value usually is the position of the data * object in a list for which a template is instantiated multiply. * * @param {Object} data The new context object. * * @param {number} index Position of the new context when multiply * instantiated. (See implementation of jstSelect().) * * @param {number} count The total number of contexts that were multiply * instantiated. (See implementation of jstSelect().) * * @return {JsEvalContext} */ JsEvalContext.prototype.clone = function(data, index, count) { var ret = JsEvalContext.create(data, this); ret.setVariable(VAR_index, index); ret.setVariable(VAR_count, count); return ret; }; /** * Binds a local variable to the given value. If set from jstemplate * jsvalue expressions, variable names must start with $, but in the * API they only have to be valid javascript identifier. * * @param {string} name * * @param {Object?} value */ JsEvalContext.prototype.setVariable = function(name, value) { this.vars_[name] = value; }; /** * Returns the value bound to the local variable of the given name, or * undefined if it wasn't set. There is no way to distinguish a * variable that wasn't set from a variable that was set to * undefined. Used mostly for testing. * * @param {string} name * * @return {Object?} value */ JsEvalContext.prototype.getVariable = function(name) { return this.vars_[name]; }; /** * Evaluates a string expression within the scope of this context * and returns the result. * * @param {string} expr A javascript expression * @param {Element} opt_template An optional node to serve as "this" * * @return {Object?} value */ JsEvalContext.prototype.evalExpression = function(expr, opt_template) { var exprFunction = jsEvalToFunction(expr); return this.jsexec(exprFunction, opt_template); }; /** * Uninlined string literals for jsEvalToFunction() (IE6 perf). */ var STRING_a = 'a_'; var STRING_b = 'b_'; var STRING_with = 'with (a_) with (b_) return '; /** * Cache for jsEvalToFunction results. * @type Object */ JsEvalContext.evalToFunctionCache_ = {}; /** * Evaluates the given expression as the body of a function that takes * vars and data as arguments. Since the resulting function depends * only on expr, we cache the result so we save some Function * invocations, and some object creations in IE6. * * @param {string} expr A javascript expression. * * @return {Function} A function that returns the value of expr in the * context of vars and data. */ function jsEvalToFunction(expr) { if (!JsEvalContext.evalToFunctionCache_[expr]) { try { // NOTE(mesch): The Function constructor is faster than eval(). JsEvalContext.evalToFunctionCache_[expr] = new Function(STRING_a, STRING_b, STRING_with + expr); } catch (e) { log('jsEvalToFunction (' + expr + ') EXCEPTION ' + e); } } return JsEvalContext.evalToFunctionCache_[expr]; } /** * Evaluates the given expression to itself. This is meant to pass * through string attribute values. * * @param {string} expr * * @return {string} */ function jsEvalToSelf(expr) { return expr; } /** * Parses the value of the jsvalues attribute in jstemplates: splits * it up into a map of labels and expressions, and creates functions * from the expressions that are suitable for execution by * JsEvalContext.jsexec(). All that is returned as a flattened array * of pairs of a String and a Function. * * @param {string} expr * * @return {Array} */ function jsEvalToValues(expr) { // TODO(mesch): It is insufficient to split the values by simply // finding semi-colons, as the semi-colon may be part of a string // constant or escaped. var ret = []; var values = expr.split(REGEXP_semicolon); for (var i = 0, I = jsLength(values); i < I; ++i) { var colon = values[i].indexOf(CHAR_colon); if (colon < 0) { continue; } var label = stringTrim(values[i].substr(0, colon)); var value = jsEvalToFunction(values[i].substr(colon + 1)); ret.push(label, value); } return ret; } /** * Parses the value of the jseval attribute of jstemplates: splits it * up into a list of expressions, and creates functions from the * expressions that are suitable for execution by * JsEvalContext.jsexec(). All that is returned as an Array of * Function. * * @param {string} expr * * @return {Array.<Function>} */ function jsEvalToExpressions(expr) { var ret = []; var values = expr.split(REGEXP_semicolon); for (var i = 0, I = jsLength(values); i < I; ++i) { if (values[i]) { var value = jsEvalToFunction(values[i]); ret.push(value); } } return ret; }