Home | History | Annotate | Download | only in inspector
      1 /**
      2  * @fileoverview This file contains miscellaneous basic functionality.
      3  *
      4  */
      5 
      6 /**
      7  * Creates a DOM element with the given tag name in the document of the
      8  * owner element.
      9  *
     10  * @param {String} tagName  The name of the tag to create.
     11  * @param {Element} owner The intended owner (i.e., parent element) of
     12  * the created element.
     13  * @param {Point} opt_position  The top-left corner of the created element.
     14  * @param {Size} opt_size  The size of the created element.
     15  * @param {Boolean} opt_noAppend Do not append the new element to the owner.
     16  * @return {Element}  The newly created element node.
     17  */
     18 function createElement(tagName, owner, opt_position, opt_size, opt_noAppend) {
     19   var element = ownerDocument(owner).createElement(tagName);
     20   if (opt_position) {
     21     setPosition(element, opt_position);
     22   }
     23   if (opt_size) {
     24     setSize(element, opt_size);
     25   }
     26   if (owner && !opt_noAppend) {
     27     appendChild(owner, element);
     28   }
     29 
     30   return element;
     31 }
     32 
     33 /**
     34  * Creates a text node with the given value.
     35  *
     36  * @param {String} value  The text to place in the new node.
     37  * @param {Element} owner The owner (i.e., parent element) of the new
     38  * text node.
     39  * @return {Text}  The newly created text node.
     40  */
     41 function createTextNode(value, owner) {
     42   var element = ownerDocument(owner).createTextNode(value);
     43   if (owner) {
     44     appendChild(owner, element);
     45   }
     46   return element;
     47 }
     48 
     49 /**
     50  * Returns the document owner of the given element. In particular,
     51  * returns window.document if node is null or the browser does not
     52  * support ownerDocument.
     53  *
     54  * @param {Node} node  The node whose ownerDocument is required.
     55  * @returns {Document|Null}  The owner document or null if unsupported.
     56  */
     57 function ownerDocument(node) {
     58   return (node ? node.ownerDocument : null) || document;
     59 }
     60 
     61 /**
     62  * Wrapper function to create CSS units (pixels) string
     63  *
     64  * @param {Number} numPixels  Number of pixels, may be floating point.
     65  * @returns {String}  Corresponding CSS units string.
     66  */
     67 function px(numPixels) {
     68   return round(numPixels) + "px";
     69 }
     70 
     71 /**
     72  * Sets the left and top of the given element to the given point.
     73  *
     74  * @param {Element} element  The dom element to manipulate.
     75  * @param {Point} point  The desired position.
     76  */
     77 function setPosition(element, point) {
     78   var style = element.style;
     79   style.position = "absolute";
     80   style.left = px(point.x);
     81   style.top = px(point.y);
     82 }
     83 
     84 /**
     85  * Sets the width and height style attributes to the given size.
     86  *
     87  * @param {Element} element  The dom element to manipulate.
     88  * @param {Size} size  The desired size.
     89  */
     90 function setSize(element, size) {
     91   var style = element.style;
     92   style.width = px(size.width);
     93   style.height = px(size.height);
     94 }
     95 
     96 /**
     97  * Sets display to none. Doing this as a function saves a few bytes for
     98  * the 'style.display' property and the 'none' literal.
     99  *
    100  * @param {Element} node  The dom element to manipulate.
    101  */
    102 function displayNone(node) {
    103   node.style.display = 'none';
    104 }
    105 
    106 /**
    107  * Sets display to default.
    108  *
    109  * @param {Element} node  The dom element to manipulate.
    110  */
    111 function displayDefault(node) {
    112   node.style.display = '';
    113 }
    114 
    115 /**
    116  * Appends the given child to the given parent in the DOM
    117  *
    118  * @param {Element} parent  The parent dom element.
    119  * @param {Node} child  The new child dom node.
    120  */
    121 function appendChild(parent, child) {
    122   parent.appendChild(child);
    123 }
    124 
    125 
    126 /**
    127  * Wrapper for the eval() builtin function to evaluate expressions and
    128  * obtain their value. It wraps the expression in parentheses such
    129  * that object literals are really evaluated to objects. Without the
    130  * wrapping, they are evaluated as block, and create syntax
    131  * errors. Also protects against other syntax errors in the eval()ed
    132  * code and returns null if the eval throws an exception.
    133  *
    134  * @param {String} expr
    135  * @return {Object|Null}
    136  */
    137 function jsEval(expr) {
    138   try {
    139     return eval('[' + expr + '][0]');
    140   } catch (e) {
    141     return null;
    142   }
    143 }
    144 
    145 
    146 /**
    147  * Wrapper for the eval() builtin function to execute statements. This
    148  * guards against exceptions thrown, but doesn't return a
    149  * value. Still, mostly for testability, it returns a boolean to
    150  * indicate whether execution was successful. NOTE:
    151  * javascript's eval semantics is murky in that it confounds
    152  * expression evaluation and statement execution into a single
    153  * construct. Cf. jsEval().
    154  *
    155  * @param {String} stmt
    156  * @return {Boolean}
    157  */
    158 function jsExec(stmt) {
    159   try {
    160     eval(stmt);
    161     return true;
    162   } catch (e) {
    163     return false;
    164   }
    165 }
    166 
    167 
    168 /**
    169  * Wrapper for eval with a context. NOTE: The style guide
    170  * deprecates eval, so this is the exception that proves the
    171  * rule. Notice also that since the value of the expression is
    172  * returned rather than assigned to a local variable, one major
    173  * objection aganist the use of the with() statement, namely that
    174  * properties of the with() target override local variables of the
    175  * same name, is void here.
    176  *
    177  * @param {String} expr
    178  * @param {Object} context
    179  * @return {Object|Null}
    180  */
    181 function jsEvalWith(expr, context) {
    182   try {
    183     with (context) {
    184       return eval('[' + expr + '][0]');
    185     }
    186   } catch (e) {
    187     return null;
    188   }
    189 }
    190 
    191 
    192 var DOM_ELEMENT_NODE = 1;
    193 var DOM_ATTRIBUTE_NODE = 2;
    194 var DOM_TEXT_NODE = 3;
    195 var DOM_CDATA_SECTION_NODE = 4;
    196 var DOM_ENTITY_REFERENCE_NODE = 5;
    197 var DOM_ENTITY_NODE = 6;
    198 var DOM_PROCESSING_INSTRUCTION_NODE = 7;
    199 var DOM_COMMENT_NODE = 8;
    200 var DOM_DOCUMENT_NODE = 9;
    201 var DOM_DOCUMENT_TYPE_NODE = 10;
    202 var DOM_DOCUMENT_FRAGMENT_NODE = 11;
    203 var DOM_NOTATION_NODE = 12;
    204 
    205 /**
    206  * Traverses the element nodes in the DOM tree underneath the given
    207  * node and finds the first node with elemId, or null if there is no such
    208  * element.  Traversal is in depth-first order.
    209  *
    210  * NOTE: The reason this is not combined with the elem() function is
    211  * that the implementations are different.
    212  * elem() is a wrapper for the built-in document.getElementById() function,
    213  * whereas this function performs the traversal itself.
    214  * Modifying elem() to take an optional root node is a possibility,
    215  * but the in-built function would perform better than using our own traversal.
    216  *
    217  * @param {Element} node Root element of subtree to traverse.
    218  * @param {String} elemId The id of the element to search for.
    219  * @return {Element|Null} The corresponding element, or null if not found.
    220  */
    221 function nodeGetElementById(node, elemId) {
    222   for (var c = node.firstChild; c; c = c.nextSibling) {
    223     if (c.id == elemId) {
    224       return c;
    225     }
    226     if (c.nodeType == DOM_ELEMENT_NODE) {
    227       var n = arguments.callee.call(this, c, elemId);
    228       if (n) {
    229         return n;
    230       }
    231     }
    232   }
    233   return null;
    234 }
    235 
    236 
    237 /**
    238  * Get an attribute from the DOM.  Simple redirect, exists to compress code.
    239  *
    240  * @param {Element} node  Element to interrogate.
    241  * @param {String} name  Name of parameter to extract.
    242  * @return {String}  Resulting attribute.
    243  */
    244 function domGetAttribute(node, name) {
    245   return node.getAttribute(name);
    246 }
    247 
    248 /**
    249  * Set an attribute in the DOM.  Simple redirect to compress code.
    250  *
    251  * @param {Element} node  Element to interrogate.
    252  * @param {String} name  Name of parameter to set.
    253  * @param {String} value  Set attribute to this value.
    254  */
    255 function domSetAttribute(node, name, value) {
    256   node.setAttribute(name, value);
    257 }
    258 
    259 /**
    260  * Remove an attribute from the DOM.  Simple redirect to compress code.
    261  *
    262  * @param {Element} node  Element to interrogate.
    263  * @param {String} name  Name of parameter to remove.
    264  */
    265 function domRemoveAttribute(node, name) {
    266   node.removeAttribute(name);
    267 }
    268 
    269 /**
    270  * Clone a node in the DOM.
    271  *
    272  * @param {Node} node  Node to clone.
    273  * @return {Node}  Cloned node.
    274  */
    275 function domCloneNode(node) {
    276   return node.cloneNode(true);
    277 }
    278 
    279 
    280 /**
    281  * Return a safe string for the className of a node.
    282  * If className is not a string, returns "".
    283  *
    284  * @param {Element} node  DOM element to query.
    285  * @return {String}
    286  */
    287 function domClassName(node) {
    288   return node.className ? "" + node.className : "";
    289 }
    290 
    291 /**
    292  * Adds a class name to the class attribute of the given node.
    293  *
    294  * @param {Element} node  DOM element to modify.
    295  * @param {String} className  Class name to add.
    296  */
    297 function domAddClass(node, className) {
    298   var name = domClassName(node);
    299   if (name) {
    300     var cn = name.split(/\s+/);
    301     var found = false;
    302     for (var i = 0; i < jsLength(cn); ++i) {
    303       if (cn[i] == className) {
    304         found = true;
    305         break;
    306       }
    307     }
    308 
    309     if (!found) {
    310       cn.push(className);
    311     }
    312 
    313     node.className = cn.join(' ');
    314   } else {
    315     node.className = className;
    316   }
    317 }
    318 
    319 /**
    320  * Removes a class name from the class attribute of the given node.
    321  *
    322  * @param {Element} node  DOM element to modify.
    323  * @param {String} className  Class name to remove.
    324  */
    325 function domRemoveClass(node, className) {
    326   var c = domClassName(node);
    327   if (!c || c.indexOf(className) == -1) {
    328     return;
    329   }
    330   var cn = c.split(/\s+/);
    331   for (var i = 0; i < jsLength(cn); ++i) {
    332     if (cn[i] == className) {
    333       cn.splice(i--, 1);
    334     }
    335   }
    336   node.className = cn.join(' ');
    337 }
    338 
    339 /**
    340  * Checks if a node belongs to a style class.
    341  *
    342  * @param {Element} node  DOM element to test.
    343  * @param {String} className  Class name to check for.
    344  * @return {Boolean}  Node belongs to style class.
    345  */
    346 function domTestClass(node, className) {
    347   var cn = domClassName(node).split(/\s+/);
    348   for (var i = 0; i < jsLength(cn); ++i) {
    349     if (cn[i] == className) {
    350       return true;
    351     }
    352   }
    353   return false;
    354 }
    355 
    356 /**
    357  * Inserts a new child before a given sibling.
    358  *
    359  * @param {Node} newChild  Node to insert.
    360  * @param {Node} oldChild  Sibling node.
    361  * @return {Node}  Reference to new child.
    362  */
    363 function domInsertBefore(newChild, oldChild) {
    364   return oldChild.parentNode.insertBefore(newChild, oldChild);
    365 }
    366 
    367 /**
    368  * Appends a new child to the specified (parent) node.
    369  *
    370  * @param {Element} node  Parent element.
    371  * @param {Node} child  Child node to append.
    372  * @return {Node}  Newly appended node.
    373  */
    374 function domAppendChild(node, child) {
    375   return node.appendChild(child);
    376 }
    377 
    378 /**
    379  * Remove a new child from the specified (parent) node.
    380  *
    381  * @param {Element} node  Parent element.
    382  * @param {Node} child  Child node to remove.
    383  * @return {Node}  Removed node.
    384  */
    385 function domRemoveChild(node, child) {
    386   return node.removeChild(child);
    387 }
    388 
    389 /**
    390  * Replaces an old child node with a new child node.
    391  *
    392  * @param {Node} newChild  New child to append.
    393  * @param {Node} oldChild  Old child to remove.
    394  * @return {Node}  Replaced node.
    395  */
    396 function domReplaceChild(newChild, oldChild) {
    397   return oldChild.parentNode.replaceChild(newChild, oldChild);
    398 }
    399 
    400 /**
    401  * Removes a node from the DOM.
    402  *
    403  * @param {Node} node  The node to remove.
    404  * @return {Node}  The removed node.
    405  */
    406 function domRemoveNode(node) {
    407   return domRemoveChild(node.parentNode, node);
    408 }
    409 
    410 /**
    411  * Creates a new text node in the given document.
    412  *
    413  * @param {Document} doc  Target document.
    414  * @param {String} text  Text composing new text node.
    415  * @return {Text}  Newly constructed text node.
    416  */
    417 function domCreateTextNode(doc, text) {
    418   return doc.createTextNode(text);
    419 }
    420 
    421 /**
    422  * Creates a new node in the given document
    423  *
    424  * @param {Document} doc  Target document.
    425  * @param {String} name  Name of new element (i.e. the tag name)..
    426  * @return {Element}  Newly constructed element.
    427  */
    428 function domCreateElement(doc, name) {
    429   return doc.createElement(name);
    430 }
    431 
    432 /**
    433  * Creates a new attribute in the given document.
    434  *
    435  * @param {Document} doc  Target document.
    436  * @param {String} name  Name of new attribute.
    437  * @return {Attr}  Newly constructed attribute.
    438  */
    439 function domCreateAttribute(doc, name) {
    440   return doc.createAttribute(name);
    441 }
    442 
    443 /**
    444  * Creates a new comment in the given document.
    445  *
    446  * @param {Document} doc  Target document.
    447  * @param {String} text  Comment text.
    448  * @return {Comment}  Newly constructed comment.
    449  */
    450 function domCreateComment(doc, text) {
    451   return doc.createComment(text);
    452 }
    453 
    454 /**
    455  * Creates a document fragment.
    456  *
    457  * @param {Document} doc  Target document.
    458  * @return {DocumentFragment}  Resulting document fragment node.
    459  */
    460 function domCreateDocumentFragment(doc) {
    461   return doc.createDocumentFragment();
    462 }
    463 
    464 /**
    465  * Redirect to document.getElementById
    466  *
    467  * @param {Document} doc  Target document.
    468  * @param {String} id  Id of requested node.
    469  * @return {Element|Null}  Resulting element.
    470  */
    471 function domGetElementById(doc, id) {
    472   return doc.getElementById(id);
    473 }
    474 
    475 /**
    476  * Redirect to window.setInterval
    477  *
    478  * @param {Window} win  Target window.
    479  * @param {Function} fun  Callback function.
    480  * @param {Number} time  Time in milliseconds.
    481  * @return {Object}  Contract id.
    482  */
    483 function windowSetInterval(win, fun, time) {
    484   return win.setInterval(fun, time);
    485 }
    486 
    487 /**
    488  * Redirect to window.clearInterval
    489  *
    490  * @param {Window} win  Target window.
    491  * @param {object} id  Contract id.
    492  * @return {any}  NOTE: Return type unknown?
    493  */
    494 function windowClearInterval(win, id) {
    495   return win.clearInterval(id);
    496 }
    497 
    498 /**
    499  * Determines whether one node is recursively contained in another.
    500  * @param parent The parent node.
    501  * @param child The node to look for in parent.
    502  * @return parent recursively contains child
    503  */
    504 function containsNode(parent, child) {
    505   while (parent != child && child.parentNode) {
    506     child = child.parentNode;
    507   }
    508   return parent == child;
    509 };
    510 /**
    511  * @fileoverview This file contains javascript utility functions that
    512  * do not depend on anything defined elsewhere.
    513  *
    514  */
    515 
    516 /**
    517  * Returns the value of the length property of the given object. Used
    518  * to reduce compiled code size.
    519  *
    520  * @param {Array | String} a  The string or array to interrogate.
    521  * @return {Number}  The value of the length property.
    522  */
    523 function jsLength(a) {
    524   return a.length;
    525 }
    526 
    527 var min = Math.min;
    528 var max = Math.max;
    529 var ceil = Math.ceil;
    530 var floor = Math.floor;
    531 var round = Math.round;
    532 var abs = Math.abs;
    533 
    534 /**
    535  * Copies all properties from second object to the first.  Modifies to.
    536  *
    537  * @param {Object} to  The target object.
    538  * @param {Object} from  The source object.
    539  */
    540 function copyProperties(to, from) {
    541   foreachin(from, function(p) {
    542     to[p] = from[p];
    543   });
    544 }
    545 
    546 /**
    547  * Iterates over the array, calling the given function for each
    548  * element.
    549  *
    550  * @param {Array} array
    551  * @param {Function} fn
    552  */
    553 function foreach(array, fn) {
    554   var I = jsLength(array);
    555   for (var i = 0; i < I; ++i) {
    556     fn(array[i], i);
    557   }
    558 }
    559 
    560 /**
    561  * Safely iterates over all properties of the given object, calling
    562  * the given function for each property. If opt_all isn't true, uses
    563  * hasOwnProperty() to assure the property is on the object, not on
    564  * its prototype.
    565  *
    566  * @param {Object} object
    567  * @param {Function} fn
    568  * @param {Boolean} opt_all  If true, also iterates over inherited properties.
    569  */
    570 function foreachin(object, fn, opt_all) {
    571   for (var i in object) {
    572     if (opt_all || !object.hasOwnProperty || object.hasOwnProperty(i)) {
    573       fn(i, object[i]);
    574     }
    575   }
    576 }
    577 
    578 /**
    579  * Appends the second array to the first, copying its elements.
    580  * Optionally only a slice of the second array is copied.
    581  *
    582  * @param {Array} a1  Target array (modified).
    583  * @param {Array} a2  Source array.
    584  * @param {Number} opt_begin  Begin of slice of second array (optional).
    585  * @param {Number} opt_end  End (exclusive) of slice of second array (optional).
    586  */
    587 function arrayAppend(a1, a2, opt_begin, opt_end) {
    588   var i0 = opt_begin || 0;
    589   var i1 = opt_end || jsLength(a2);
    590   for (var i = i0; i < i1; ++i) {
    591     a1.push(a2[i]);
    592   }
    593 }
    594 
    595 /**
    596  * Trim whitespace from begin and end of string.
    597  *
    598  * @see testStringTrim();
    599  *
    600  * @param {String} str  Input string.
    601  * @return {String}  Trimmed string.
    602  */
    603 function stringTrim(str) {
    604   return stringTrimRight(stringTrimLeft(str));
    605 }
    606 
    607 /**
    608  * Trim whitespace from beginning of string.
    609  *
    610  * @see testStringTrimLeft();
    611  *
    612  * @param {String} str  Input string.
    613  * @return {String}  Trimmed string.
    614  */
    615 function stringTrimLeft(str) {
    616   return str.replace(/^\s+/, "");
    617 }
    618 
    619 /**
    620  * Trim whitespace from end of string.
    621  *
    622  * @see testStringTrimRight();
    623  *
    624  * @param {String} str  Input string.
    625  * @return {String}  Trimmed string.
    626  */
    627 function stringTrimRight(str) {
    628   return str.replace(/\s+$/, "");
    629 }
    630 
    631 /**
    632  * Jscompiler wrapper for parseInt() with base 10.
    633  *
    634  * @param {String} s String repersentation of a number.
    635  *
    636  * @return {Number} The integer contained in s, converted on base 10.
    637  */
    638 function parseInt10(s) {
    639   return parseInt(s, 10);
    640 }
    641 /**
    642  * @fileoverview A simple formatter to project JavaScript data into
    643  * HTML templates. The template is edited in place. I.e. in order to
    644  * instantiate a template, clone it from the DOM first, and then
    645  * process the cloned template. This allows for updating of templates:
    646  * If the templates is processed again, changed values are merely
    647  * updated.
    648  *
    649  * NOTE: IE DOM doesn't have importNode().
    650  *
    651  * NOTE: The property name "length" must not be used in input
    652  * data, see comment in jstSelect_().
    653  */
    654 
    655 
    656 /**
    657  * Names of jstemplate attributes. These attributes are attached to
    658  * normal HTML elements and bind expression context data to the HTML
    659  * fragment that is used as template.
    660  */
    661 var ATT_select = 'jsselect';
    662 var ATT_instance = 'jsinstance';
    663 var ATT_display = 'jsdisplay';
    664 var ATT_values = 'jsvalues';
    665 var ATT_eval = 'jseval';
    666 var ATT_transclude = 'transclude';
    667 var ATT_content = 'jscontent';
    668 
    669 
    670 /**
    671  * Names of special variables defined by the jstemplate evaluation
    672  * context. These can be used in js expression in jstemplate
    673  * attributes.
    674  */
    675 var VAR_index = '$index';
    676 var VAR_this = '$this';
    677 
    678 
    679 /**
    680  * Context for processing a jstemplate. The context contains a context
    681  * object, whose properties can be referred to in jstemplate
    682  * expressions, and it holds the locally defined variables.
    683  *
    684  * @param {Object} opt_data The context object. Null if no context.
    685  *
    686  * @param {Object} opt_parent The parent context, from which local
    687  * variables are inherited. Normally the context object of the parent
    688  * context is the object whose property the parent object is. Null for the
    689  * context of the root object.
    690  *
    691  * @constructor
    692  */
    693 function JsExprContext(opt_data, opt_parent) {
    694   var me = this;
    695 
    696   /**
    697    * The local context of the input data in which the jstemplate
    698    * expressions are evaluated. Notice that this is usually an Object,
    699    * but it can also be a scalar value (and then still the expression
    700    * $this can be used to refer to it). Notice this can be a scalar
    701    * value, including undefined.
    702    *
    703    * @type {Object}
    704    */
    705   me.data_ = opt_data;
    706 
    707   /**
    708    * The context for variable definitions in which the jstemplate
    709    * expressions are evaluated. Other than for the local context,
    710    * which replaces the parent context, variable definitions of the
    711    * parent are inherited. The special variable $this points to data_.
    712    *
    713    * @type {Object}
    714    */
    715   me.vars_ = {};
    716   if (opt_parent) {
    717     copyProperties(me.vars_, opt_parent.vars_);
    718   }
    719   this.vars_[VAR_this] = me.data_;
    720 }
    721 
    722 
    723 /**
    724  * Evaluates the given expression in the context of the current
    725  * context object and the current local variables.
    726  *
    727  * @param {String} expr A javascript expression.
    728  *
    729  * @param {Element} template DOM node of the template.
    730  *
    731  * @return The value of that expression.
    732  */
    733 JsExprContext.prototype.jseval = function(expr, template) {
    734   with (this.vars_) {
    735     with (this.data_) {
    736       try {
    737         return (function() {
    738           return eval('[' + expr + '][0]');
    739         }).call(template);
    740       } catch (e) {
    741         return null;
    742       }
    743     }
    744   }
    745 }
    746 
    747 
    748 /**
    749  * Clones the current context for a new context object. The cloned
    750  * context has the data object as its context object and the current
    751  * context as its parent context. It also sets the $index variable to
    752  * the given value. This value usually is the position of the data
    753  * object in a list for which a template is instantiated multiply.
    754  *
    755  * @param {Object} data The new context object.
    756  *
    757  * @param {Number} index Position of the new context when multiply
    758  * instantiated. (See implementation of jstSelect().)
    759  *
    760  * @return {JsExprContext}
    761  */
    762 JsExprContext.prototype.clone = function(data, index) {
    763   var ret = new JsExprContext(data, this);
    764   ret.setVariable(VAR_index, index);
    765   if (this.resolver_) {
    766     ret.setSubTemplateResolver(this.resolver_);
    767   }
    768   return ret;
    769 }
    770 
    771 
    772 /**
    773  * Binds a local variable to the given value. If set from jstemplate
    774  * jsvalue expressions, variable names must start with $, but in the
    775  * API they only have to be valid javascript identifier.
    776  *
    777  * @param {String} name
    778  *
    779  * @param {Object} value
    780  */
    781 JsExprContext.prototype.setVariable = function(name, value) {
    782   this.vars_[name] = value;
    783 }
    784 
    785 
    786 /**
    787  * Sets the function used to resolve the values of the transclude
    788  * attribute into DOM nodes. By default, this is jstGetTemplate(). The
    789  * value set here is inherited by clones of this context.
    790  *
    791  * @param {Function} resolver The function used to resolve transclude
    792  * ids into a DOM node of a subtemplate. The DOM node returned by this
    793  * function will be inserted into the template instance being
    794  * processed. Thus, the resolver function must instantiate the
    795  * subtemplate as necessary.
    796  */
    797 JsExprContext.prototype.setSubTemplateResolver = function(resolver) {
    798   this.resolver_ = resolver;
    799 }
    800 
    801 
    802 /**
    803  * Resolves a sub template from an id. Used to process the transclude
    804  * attribute. If a resolver function was set using
    805  * setSubTemplateResolver(), it will be used, otherwise
    806  * jstGetTemplate().
    807  *
    808  * @param {String} id The id of the sub template.
    809  *
    810  * @return {Node} The root DOM node of the sub template, for direct
    811  * insertion into the currently processed template instance.
    812  */
    813 JsExprContext.prototype.getSubTemplate = function(id) {
    814   return (this.resolver_ || jstGetTemplate).call(this, id);
    815 }
    816 
    817 
    818 /**
    819  * HTML template processor. Data values are bound to HTML templates
    820  * using the attributes transclude, jsselect, jsdisplay, jscontent,
    821  * jsvalues. The template is modifed in place. The values of those
    822  * attributes are JavaScript expressions that are evaluated in the
    823  * context of the data object fragment.
    824  *
    825  * @param {JsExprContext} context Context created from the input data
    826  * object.
    827  *
    828  * @param {Element} template DOM node of the template. This will be
    829  * processed in place. After processing, it will still be a valid
    830  * template that, if processed again with the same data, will remain
    831  * unchanged.
    832  */
    833 function jstProcess(context, template) {
    834   var processor = new JstProcessor();
    835   processor.run_([ processor, processor.jstProcess_, context, template ]);
    836 }
    837 
    838 
    839 /**
    840  * Internal class used by jstemplates to maintain context.
    841  * NOTE: This is necessary to process deep templates in Safari
    842  * which has a relatively shallow stack.
    843  * @class
    844  */
    845 function JstProcessor() {
    846 }
    847 
    848 
    849 /**
    850  * Runs the state machine, beginning with function "start".
    851  *
    852  * @param {Array} start The first function to run, in the form
    853  * [object, method, args ...]
    854  */
    855 JstProcessor.prototype.run_ = function(start) {
    856   var me = this;
    857 
    858   me.queue_ = [ start ];
    859   while (jsLength(me.queue_)) {
    860     var f = me.queue_.shift();
    861     f[1].apply(f[0], f.slice(2));
    862   }
    863 }
    864 
    865 
    866 /**
    867  * Appends a function to be called later.
    868  * Analogous to calling that function on a subsequent line, or a subsequent
    869  * iteration of a loop.
    870  *
    871  * @param {Array} f  A function in the form [object, method, args ...]
    872  */
    873 JstProcessor.prototype.enqueue_ = function(f) {
    874   this.queue_.push(f);
    875 }
    876 
    877 
    878 /**
    879  * Implements internals of jstProcess.
    880  *
    881  * @param {JsExprContext} context
    882  *
    883  * @param {Element} template
    884  */
    885 JstProcessor.prototype.jstProcess_ = function(context, template) {
    886   var me = this;
    887 
    888   var transclude = domGetAttribute(template, ATT_transclude);
    889   if (transclude) {
    890     var tr = context.getSubTemplate(transclude);
    891     if (tr) {
    892       domReplaceChild(tr, template);
    893       me.enqueue_([ me, me.jstProcess_, context, tr ]);
    894     } else {
    895       domRemoveNode(template);
    896     }
    897     return;
    898   }
    899 
    900   var select = domGetAttribute(template, ATT_select);
    901   if (select) {
    902     me.jstSelect_(context, template, select);
    903     return;
    904   }
    905 
    906   var display = domGetAttribute(template, ATT_display);
    907   if (display) {
    908     if (!context.jseval(display, template)) {
    909       displayNone(template);
    910       return;
    911     }
    912 
    913     displayDefault(template);
    914   }
    915 
    916 
    917   var values = domGetAttribute(template, ATT_values);
    918   if (values) {
    919     me.jstValues_(context, template, values);
    920   }
    921 
    922   var expressions = domGetAttribute(template, ATT_eval);
    923   if (expressions) {
    924     foreach(expressions.split(/\s*;\s*/), function(expression) {
    925       expression = stringTrim(expression);
    926       if (jsLength(expression)) {
    927         context.jseval(expression, template);
    928       }
    929     });
    930   }
    931 
    932   var content = domGetAttribute(template, ATT_content);
    933   if (content) {
    934     me.jstContent_(context, template, content);
    935 
    936   } else {
    937     var childnodes = [];
    938     for (var i = 0; i < jsLength(template.childNodes); ++i) {
    939       if (template.childNodes[i].nodeType == DOM_ELEMENT_NODE) {
    940       me.enqueue_(
    941           [ me, me.jstProcess_, context, template.childNodes[i] ]);
    942       }
    943     }
    944   }
    945 }
    946 
    947 
    948 /**
    949  * Implements the jsselect attribute: evalutes the value of the
    950  * jsselect attribute in the current context, with the current
    951  * variable bindings (see JsExprContext.jseval()). If the value is an
    952  * array, the current template node is multiplied once for every
    953  * element in the array, with the array element being the context
    954  * object. If the array is empty, or the value is undefined, then the
    955  * current template node is dropped. If the value is not an array,
    956  * then it is just made the context object.
    957  *
    958  * @param {JsExprContext} context The current evaluation context.
    959  *
    960  * @param {Element} template The currently processed node of the template.
    961  *
    962  * @param {String} select The javascript expression to evaluate.
    963  *
    964  * @param {Function} process The function to continue processing with.
    965  */
    966 JstProcessor.prototype.jstSelect_ = function(context, template, select) {
    967   var me = this;
    968 
    969   var value = context.jseval(select, template);
    970   domRemoveAttribute(template, ATT_select);
    971 
    972   var instance = domGetAttribute(template, ATT_instance);
    973   var instance_last = false;
    974   if (instance) {
    975     if (instance.charAt(0) == '*') {
    976       instance = parseInt10(instance.substr(1));
    977       instance_last = true;
    978     } else {
    979       instance = parseInt10(instance);
    980     }
    981   }
    982 
    983   var multiple = (value !== null &&
    984                   typeof value == 'object' &&
    985                   typeof value.length == 'number');
    986   var multiple_empty = (multiple && value.length == 0);
    987 
    988   if (multiple) {
    989     if (multiple_empty) {
    990       if (!instance) {
    991         domSetAttribute(template, ATT_select, select);
    992         domSetAttribute(template, ATT_instance, '*0');
    993         displayNone(template);
    994       } else {
    995         domRemoveNode(template);
    996       }
    997 
    998     } else {
    999       displayDefault(template);
   1000       if (instance === null || instance === "" || instance === undefined ||
   1001           (instance_last && instance < jsLength(value) - 1)) {
   1002         var templatenodes = [];
   1003         var instances_start = instance || 0;
   1004         for (var i = instances_start + 1; i < jsLength(value); ++i) {
   1005           var node = domCloneNode(template);
   1006           templatenodes.push(node);
   1007           domInsertBefore(node, template);
   1008         }
   1009         templatenodes.push(template);
   1010 
   1011         for (var i = 0; i < jsLength(templatenodes); ++i) {
   1012           var ii = i + instances_start;
   1013           var v = value[ii];
   1014           var t = templatenodes[i];
   1015 
   1016           me.enqueue_([ me, me.jstProcess_, context.clone(v, ii), t ]);
   1017           var instanceStr = (ii == jsLength(value) - 1 ? '*' : '') + ii;
   1018           me.enqueue_(
   1019               [ null, postProcessMultiple_, t, select, instanceStr ]);
   1020         }
   1021 
   1022       } else if (instance < jsLength(value)) {
   1023         var v = value[instance];
   1024 
   1025         me.enqueue_(
   1026             [me, me.jstProcess_, context.clone(v, instance), template]);
   1027         var instanceStr = (instance == jsLength(value) - 1 ? '*' : '')
   1028                           + instance;
   1029         me.enqueue_(
   1030             [ null, postProcessMultiple_, template, select, instanceStr ]);
   1031       } else {
   1032         domRemoveNode(template);
   1033       }
   1034     }
   1035   } else {
   1036     if (value == null) {
   1037       domSetAttribute(template, ATT_select, select);
   1038       displayNone(template);
   1039     } else {
   1040       me.enqueue_(
   1041           [ me, me.jstProcess_, context.clone(value, 0), template ]);
   1042       me.enqueue_(
   1043           [ null, postProcessSingle_, template, select ]);
   1044     }
   1045   }
   1046 }
   1047 
   1048 
   1049 /**
   1050  * Sets ATT_select and ATT_instance following recursion to jstProcess.
   1051  *
   1052  * @param {Element} template  The template
   1053  *
   1054  * @param {String} select  The jsselect string
   1055  *
   1056  * @param {String} instanceStr  The new value for the jsinstance attribute
   1057  */
   1058 function postProcessMultiple_(template, select, instanceStr) {
   1059   domSetAttribute(template, ATT_select, select);
   1060   domSetAttribute(template, ATT_instance, instanceStr);
   1061 }
   1062 
   1063 
   1064 /**
   1065  * Sets ATT_select and makes the element visible following recursion to
   1066  * jstProcess.
   1067  *
   1068  * @param {Element} template  The template
   1069  *
   1070  * @param {String} select  The jsselect string
   1071  */
   1072 function postProcessSingle_(template, select) {
   1073   domSetAttribute(template, ATT_select, select);
   1074   displayDefault(template);
   1075 }
   1076 
   1077 
   1078 /**
   1079  * Implements the jsvalues attribute: evaluates each of the values and
   1080  * assigns them to variables in the current context (if the name
   1081  * starts with '$', javascript properties of the current template node
   1082  * (if the name starts with '.'), or DOM attributes of the current
   1083  * template node (otherwise). Since DOM attribute values are always
   1084  * strings, the value is coerced to string in the latter case,
   1085  * otherwise it's the uncoerced javascript value.
   1086  *
   1087  * @param {JsExprContext} context Current evaluation context.
   1088  *
   1089  * @param {Element} template Currently processed template node.
   1090  *
   1091  * @param {String} valuesStr Value of the jsvalues attribute to be
   1092  * processed.
   1093  */
   1094 JstProcessor.prototype.jstValues_ = function(context, template, valuesStr) {
   1095   var values = valuesStr.split(/\s*;\s*/);
   1096   for (var i = 0; i < jsLength(values); ++i) {
   1097     var colon = values[i].indexOf(':');
   1098     if (colon < 0) {
   1099       continue;
   1100     }
   1101     var label = stringTrim(values[i].substr(0, colon));
   1102     var value = context.jseval(values[i].substr(colon + 1), template);
   1103 
   1104     if (label.charAt(0) == '$') {
   1105       context.setVariable(label, value);
   1106 
   1107     } else if (label.charAt(0) == '.') {
   1108       var nameSpaceLabel = label.substr(1).split('.');
   1109       var nameSpaceObject = template;
   1110       var nameSpaceDepth = jsLength(nameSpaceLabel);
   1111       for (var j = 0, J = nameSpaceDepth - 1; j < J; ++j) {
   1112         var jLabel = nameSpaceLabel[j];
   1113         if (!nameSpaceObject[jLabel]) {
   1114           nameSpaceObject[jLabel] = {};
   1115         }
   1116         nameSpaceObject = nameSpaceObject[jLabel];
   1117       }
   1118       nameSpaceObject[nameSpaceLabel[nameSpaceDepth - 1]] = value;
   1119     } else if (label) {
   1120       if (typeof value == 'boolean') {
   1121         if (value) {
   1122           domSetAttribute(template, label, label);
   1123         } else {
   1124           domRemoveAttribute(template, label);
   1125         }
   1126       } else {
   1127         domSetAttribute(template, label, '' + value);
   1128       }
   1129     }
   1130   }
   1131 }
   1132 
   1133 
   1134 /**
   1135  * Implements the jscontent attribute. Evalutes the expression in
   1136  * jscontent in the current context and with the current variables,
   1137  * and assigns its string value to the content of the current template
   1138  * node.
   1139  *
   1140  * @param {JsExprContext} context Current evaluation context.
   1141  *
   1142  * @param {Element} template Currently processed template node.
   1143  *
   1144  * @param {String} content Value of the jscontent attribute to be
   1145  * processed.
   1146  */
   1147 JstProcessor.prototype.jstContent_ = function(context, template, content) {
   1148   var value = '' + context.jseval(content, template);
   1149   if (template.innerHTML == value) {
   1150     return;
   1151   }
   1152   while (template.firstChild) {
   1153     domRemoveNode(template.firstChild);
   1154   }
   1155   var t = domCreateTextNode(ownerDocument(template), value);
   1156   domAppendChild(template, t);
   1157 }
   1158 
   1159 
   1160 /**
   1161  * Helps to implement the transclude attribute, and is the initial
   1162  * call to get hold of a template from its ID.
   1163  *
   1164  * @param {String} name The ID of the HTML element used as template.
   1165  *
   1166  * @returns {Element} The DOM node of the template. (Only element
   1167  * nodes can be found by ID, hence it's a Element.)
   1168  */
   1169 function jstGetTemplate(name) {
   1170   var section = domGetElementById(document, name);
   1171   if (section) {
   1172     var ret = domCloneNode(section);
   1173     domRemoveAttribute(ret, 'id');
   1174     return ret;
   1175   } else {
   1176     return null;
   1177   }
   1178 }
   1179 
   1180 window['jstGetTemplate'] = jstGetTemplate;
   1181 window['jstProcess'] = jstProcess;
   1182 window['JsExprContext'] = JsExprContext;
   1183