Home | History | Annotate | Download | only in jstemplate
      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 (a] 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 }
    410