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