| 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 |