diff options
author | arv@google.com <arv@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-06-09 23:07:26 +0000 |
---|---|---|
committer | arv@google.com <arv@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-06-09 23:07:26 +0000 |
commit | 9b115d5e9523de6f617b189e5d3bf08f59ef40a7 (patch) | |
tree | 6a6a76c24e6d6a976ae1a25e816dfb5f2c3b3442 /chrome/third_party/jstemplate/jstemplate_compiled.js | |
parent | b4530561effb48137edab5a0bceaddb5707264d1 (diff) | |
download | chromium_src-9b115d5e9523de6f617b189e5d3bf08f59ef40a7.zip chromium_src-9b115d5e9523de6f617b189e5d3bf08f59ef40a7.tar.gz chromium_src-9b115d5e9523de6f617b189e5d3bf08f59ef40a7.tar.bz2 |
Update JSTemplate to the latest version.
This version is from Google and has already stripped all non related code except for the MAPS_DEBUG flag. I manually removed all the debugging code related to MAPS_DEBUG.
TEST=New tab page, history, downloads and all other pages using HTML content should still work.
BUG=None
Review URL: http://codereview.chromium.org/119384
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@17990 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/third_party/jstemplate/jstemplate_compiled.js')
-rw-r--r-- | chrome/third_party/jstemplate/jstemplate_compiled.js | 1826 |
1 files changed, 1163 insertions, 663 deletions
diff --git a/chrome/third_party/jstemplate/jstemplate_compiled.js b/chrome/third_party/jstemplate/jstemplate_compiled.js index 2f62b31..bf5d082 100644 --- a/chrome/third_party/jstemplate/jstemplate_compiled.js +++ b/chrome/third_party/jstemplate/jstemplate_compiled.js @@ -1,194 +1,146 @@ +(function(){ /** - * @fileoverview This file contains miscellaneous basic functionality. - * + * @fileoverview Miscellaneous constants and functions referenced in + * the main source files. */ -/** - * Creates a DOM element with the given tag name in the document of the - * owner element. - * - * @param {String} tagName The name of the tag to create. - * @param {Element} owner The intended owner (i.e., parent element) of - * the created element. - * @param {Point} opt_position The top-left corner of the created element. - * @param {Size} opt_size The size of the created element. - * @param {Boolean} opt_noAppend Do not append the new element to the owner. - * @return {Element} The newly created element node. - */ -function createElement(tagName, owner, opt_position, opt_size, opt_noAppend) { - var element = ownerDocument(owner).createElement(tagName); - if (opt_position) { - setPosition(element, opt_position); - } - if (opt_size) { - setSize(element, opt_size); - } - if (owner && !opt_noAppend) { - appendChild(owner, element); - } +function log(msg) {} + +/** @const */ var STRING_empty = ''; + +/** @const */ var CSS_display = 'display'; +/** @const */ var CSS_position = 'position'; + +var TYPE_boolean = 'boolean'; +var TYPE_number = 'number'; +var TYPE_object = 'object'; +var TYPE_string = 'string'; +var TYPE_function = 'function'; +var TYPE_undefined = 'undefined'; - return element; -} /** - * Creates a text node with the given value. + * Wrapper for the eval() builtin function to evaluate expressions and + * obtain their value. It wraps the expression in parentheses such + * that object literals are really evaluated to objects. Without the + * wrapping, they are evaluated as block, and create syntax + * errors. Also protects against other syntax errors in the eval()ed + * code and returns null if the eval throws an exception. * - * @param {String} value The text to place in the new node. - * @param {Element} owner The owner (i.e., parent element) of the new - * text node. - * @return {Text} The newly created text node. + * @param {string} expr + * @return {Object|null} */ -function createTextNode(value, owner) { - var element = ownerDocument(owner).createTextNode(value); - if (owner) { - appendChild(owner, element); +function jsEval(expr) { + try { + return eval('[' + expr + '][0]'); + } catch (e) { + log('EVAL FAILED ' + expr + ': ' + e); + return null; } - return element; } -/** - * Returns the document owner of the given element. In particular, - * returns window.document if node is null or the browser does not - * support ownerDocument. - * - * @param {Node} node The node whose ownerDocument is required. - * @returns {Document|Null} The owner document or null if unsupported. - */ -function ownerDocument(node) { - return (node ? node.ownerDocument : null) || document; +function jsLength(obj) { + return obj.length; } -/** - * Wrapper function to create CSS units (pixels) string - * - * @param {Number} numPixels Number of pixels, may be floating point. - * @returns {String} Corresponding CSS units string. - */ -function px(numPixels) { - return round(numPixels) + "px"; -} +function assert(obj) {} /** - * Sets the left and top of the given element to the given point. + * Copies all properties from second object to the first. Modifies to. * - * @param {Element} element The dom element to manipulate. - * @param {Point} point The desired position. + * @param {Object} to The target object. + * @param {Object} from The source object. */ -function setPosition(element, point) { - var style = element.style; - style.position = "absolute"; - style.left = px(point.x); - style.top = px(point.y); +function copyProperties(to, from) { + for (var p in from) { + to[p] = from[p]; + } } -/** - * Sets the width and height style attributes to the given size. - * - * @param {Element} element The dom element to manipulate. - * @param {Size} size The desired size. - */ -function setSize(element, size) { - var style = element.style; - style.width = px(size.width); - style.height = px(size.height); -} /** - * Sets display to none. Doing this as a function saves a few bytes for - * the 'style.display' property and the 'none' literal. - * - * @param {Element} node The dom element to manipulate. + * @param {Object|null|undefined} value The possible value to use. + * @param {Object} defaultValue The default if the value is not set. + * @return {Object} The value, if it is + * defined and not null; otherwise the default */ -function displayNone(node) { - node.style.display = 'none'; +function getDefaultObject(value, defaultValue) { + if (typeof value != TYPE_undefined && value != null) { + return /** @type Object */(value); + } else { + return defaultValue; + } } /** - * Sets display to default. - * - * @param {Element} node The dom element to manipulate. + * Detect if an object looks like an Array. + * Note that instanceof Array is not robust; for example an Array + * created in another iframe fails instanceof Array. + * @param {Object|null} value Object to interrogate + * @return {boolean} Is the object an array? */ -function displayDefault(node) { - node.style.display = ''; +function isArray(value) { + return value != null && + typeof value == TYPE_object && + typeof value.length == TYPE_number; } + /** - * Appends the given child to the given parent in the DOM + * Finds a slice of an array. * - * @param {Element} parent The parent dom element. - * @param {Node} child The new child dom node. + * @param {Array} array Array to be sliced. + * @param {number} start The start of the slice. + * @param {number} opt_end The end of the slice (optional). + * @return {Array} array The slice of the array from start to end. */ -function appendChild(parent, child) { - parent.appendChild(child); +function arraySlice(array, start, opt_end) { + return Function.prototype.call.apply(Array.prototype.slice, arguments); } /** - * Wrapper for the eval() builtin function to evaluate expressions and - * obtain their value. It wraps the expression in parentheses such - * that object literals are really evaluated to objects. Without the - * wrapping, they are evaluated as block, and create syntax - * errors. Also protects against other syntax errors in the eval()ed - * code and returns null if the eval throws an exception. + * Jscompiler wrapper for parseInt() with base 10. * - * @param {String} expr - * @return {Object|Null} + * @param {string} s string repersentation of a number. + * + * @return {number} The integer contained in s, converted on base 10. */ -function jsEval(expr) { - try { - return eval('[' + expr + '][0]'); - } catch (e) { - return null; - } +function parseInt10(s) { + return parseInt(s, 10); } /** - * Wrapper for the eval() builtin function to execute statements. This - * guards against exceptions thrown, but doesn't return a - * value. Still, mostly for testability, it returns a boolean to - * indicate whether execution was successful. NOTE: - * javascript's eval semantics is murky in that it confounds - * expression evaluation and statement execution into a single - * construct. Cf. jsEval(). + * Clears the array by setting the length property to 0. This usually + * works, and if it should turn out not to work everywhere, here would + * be the place to implement the browser specific workaround. * - * @param {String} stmt - * @return {Boolean} + * @param {Array} array Array to be cleared. */ -function jsExec(stmt) { - try { - eval(stmt); - return true; - } catch (e) { - return false; - } +function arrayClear(array) { + array.length = 0; } /** - * Wrapper for eval with a context. NOTE: The style guide - * deprecates eval, so this is the exception that proves the - * rule. Notice also that since the value of the expression is - * returned rather than assigned to a local variable, one major - * objection aganist the use of the with() statement, namely that - * properties of the with() target override local variables of the - * same name, is void here. + * Prebinds "this" within the given method to an object, but ignores all + * arguments passed to the resulting function. + * I.e. var_args are all the arguments that method is invoked with when + * invoking the bound function. * - * @param {String} expr - * @param {Object} context - * @return {Object|Null} + * @param {Object|null} object The object that the method call targets. + * @param {Function} method The target method. + * @return {Function} Method with the target object bound to it and curried by + * the provided arguments. */ -function jsEvalWith(expr, context) { - try { - with (context) { - return eval('[' + expr + '][0]'); - } - } catch (e) { - return null; +function bindFully(object, method, var_args) { + var args = arraySlice(arguments, 2); + return function() { + return method.apply(object, args); } } - var DOM_ELEMENT_NODE = 1; var DOM_ATTRIBUTE_NODE = 2; var DOM_TEXT_NODE = 3; @@ -202,55 +154,92 @@ var DOM_DOCUMENT_TYPE_NODE = 10; var DOM_DOCUMENT_FRAGMENT_NODE = 11; var DOM_NOTATION_NODE = 12; + + +function domGetElementById(document, id) { + return document.getElementById(id); +} + /** - * Traverses the element nodes in the DOM tree underneath the given - * node and finds the first node with elemId, or null if there is no such - * element. Traversal is in depth-first order. + * Creates a new node in the given document * - * NOTE: The reason this is not combined with the elem() function is - * that the implementations are different. - * elem() is a wrapper for the built-in document.getElementById() function, - * whereas this function performs the traversal itself. - * Modifying elem() to take an optional root node is a possibility, - * but the in-built function would perform better than using our own traversal. + * @param {Document} doc Target document. + * @param {string} name Name of new element (i.e. the tag name).. + * @return {Element} Newly constructed element. + */ +function domCreateElement(doc, name) { + return doc.createElement(name); +} + +/** + * Traverses the element nodes in the DOM section underneath the given + * node and invokes the given callback as a method on every element + * node encountered. * - * @param {Element} node Root element of subtree to traverse. - * @param {String} elemId The id of the element to search for. - * @return {Element|Null} The corresponding element, or null if not found. + * @param {Element} node Parent element of the subtree to traverse. + * @param {Function} callback Called on each node in the traversal. + */ +function domTraverseElements(node, callback) { + var traverser = new DomTraverser(callback); + traverser.run(node); +} + +/** + * A class to hold state for a dom traversal. + * @param {Function} callback Called on each node in the traversal. + * @constructor + * @class + */ +function DomTraverser(callback) { + this.callback_ = callback; +} + +/** + * Processes the dom tree in breadth-first order. + * @param {Element} root The root node of the traversal. + */ +DomTraverser.prototype.run = function(root) { + var me = this; + me.queue_ = [ root ]; + while (jsLength(me.queue_)) { + me.process_(me.queue_.shift()); + } +} + +/** + * Processes a single node. + * @param {Element} node The current node of the traversal. */ -function nodeGetElementById(node, elemId) { +DomTraverser.prototype.process_ = function(node) { + var me = this; + + me.callback_(node); + for (var c = node.firstChild; c; c = c.nextSibling) { - if (c.id == elemId) { - return c; - } if (c.nodeType == DOM_ELEMENT_NODE) { - var n = arguments.callee.call(this, c, elemId); - if (n) { - return n; - } + me.queue_.push(c); } } - return null; } - /** * Get an attribute from the DOM. Simple redirect, exists to compress code. * * @param {Element} node Element to interrogate. - * @param {String} name Name of parameter to extract. - * @return {String} Resulting attribute. + * @param {string} name Name of parameter to extract. + * @return {string|null} Resulting attribute. */ function domGetAttribute(node, name) { return node.getAttribute(name); } + /** * Set an attribute in the DOM. Simple redirect to compress code. * * @param {Element} node Element to interrogate. - * @param {String} name Name of parameter to set. - * @param {String} value Set attribute to this value. + * @param {string} name Name of parameter to set. + * @param {string|number} value Set attribute to this value. */ function domSetAttribute(node, name, value) { node.setAttribute(name, value); @@ -260,7 +249,7 @@ function domSetAttribute(node, name, value) { * Remove an attribute from the DOM. Simple redirect to compress code. * * @param {Element} node Element to interrogate. - * @param {String} name Name of parameter to remove. + * @param {string} name Name of parameter to remove. */ function domRemoveAttribute(node, name) { node.removeAttribute(name); @@ -276,114 +265,96 @@ function domCloneNode(node) { return node.cloneNode(true); } - /** - * Return a safe string for the className of a node. - * If className is not a string, returns "". + * Clone a element in the DOM. * - * @param {Element} node DOM element to query. - * @return {String} + * @param {Element} element Element to clone. + * @return {Element} Cloned element. */ -function domClassName(node) { - return node.className ? "" + node.className : ""; +function domCloneElement(element) { + return /** @type {Element} */(domCloneNode(element)); } /** - * Adds a class name to the class attribute of the given node. + * Returns the document owner of the given element. In particular, + * returns window.document if node is null or the browser does not + * support ownerDocument. If the node is a document itself, returns + * itself. * - * @param {Element} node DOM element to modify. - * @param {String} className Class name to add. + * @param {Node|null|undefined} node The node whose ownerDocument is required. + * @returns {Document} The owner document or window.document if unsupported. */ -function domAddClass(node, className) { - var name = domClassName(node); - if (name) { - var cn = name.split(/\s+/); - var found = false; - for (var i = 0; i < jsLength(cn); ++i) { - if (cn[i] == className) { - found = true; - break; - } - } - - if (!found) { - cn.push(className); - } - - node.className = cn.join(' '); +function ownerDocument(node) { + if (!node) { + return document; + } else if (node.nodeType == DOM_DOCUMENT_NODE) { + return /** @type Document */(node); } else { - node.className = className; + return node.ownerDocument || document; } } /** - * Removes a class name from the class attribute of the given node. + * Creates a new text node in the given document. * - * @param {Element} node DOM element to modify. - * @param {String} className Class name to remove. + * @param {Document} doc Target document. + * @param {string} text Text composing new text node. + * @return {Text} Newly constructed text node. */ -function domRemoveClass(node, className) { - var c = domClassName(node); - if (!c || c.indexOf(className) == -1) { - return; - } - var cn = c.split(/\s+/); - for (var i = 0; i < jsLength(cn); ++i) { - if (cn[i] == className) { - cn.splice(i--, 1); - } - } - node.className = cn.join(' '); +function domCreateTextNode(doc, text) { + return doc.createTextNode(text); } /** - * Checks if a node belongs to a style class. + * Appends a new child to the specified (parent) node. * - * @param {Element} node DOM element to test. - * @param {String} className Class name to check for. - * @return {Boolean} Node belongs to style class. + * @param {Element} node Parent element. + * @param {Node} child Child node to append. + * @return {Node} Newly appended node. */ -function domTestClass(node, className) { - var cn = domClassName(node).split(/\s+/); - for (var i = 0; i < jsLength(cn); ++i) { - if (cn[i] == className) { - return true; - } - } - return false; +function domAppendChild(node, child) { + return node.appendChild(child); } /** - * Inserts a new child before a given sibling. + * Sets display to default. * - * @param {Node} newChild Node to insert. - * @param {Node} oldChild Sibling node. - * @return {Node} Reference to new child. + * @param {Element} node The dom element to manipulate. */ -function domInsertBefore(newChild, oldChild) { - return oldChild.parentNode.insertBefore(newChild, oldChild); +function displayDefault(node) { + node.style[CSS_display] = ''; } /** - * Appends a new child to the specified (parent) node. + * Sets display to none. Doing this as a function saves a few bytes for + * the 'style.display' property and the 'none' literal. * - * @param {Element} node Parent element. - * @param {Node} child Child node to append. - * @return {Node} Newly appended node. + * @param {Element} node The dom element to manipulate. */ -function domAppendChild(node, child) { - return node.appendChild(child); +function displayNone(node) { + node.style[CSS_display] = 'none'; } + /** - * Remove a new child from the specified (parent) node. + * Sets position style attribute to absolute. * - * @param {Element} node Parent element. - * @param {Node} child Child node to remove. - * @return {Node} Removed node. + * @param {Element} node The dom element to manipulate. */ -function domRemoveChild(node, child) { - return node.removeChild(child); +function positionAbsolute(node) { + node.style[CSS_position] = 'absolute'; +} + + +/** + * Inserts a new child before a given sibling. + * + * @param {Node} newChild Node to insert. + * @param {Node} oldChild Sibling node. + * @return {Node} Reference to new child. + */ +function domInsertBefore(newChild, oldChild) { + return oldChild.parentNode.insertBefore(newChild, oldChild); } /** @@ -408,237 +379,437 @@ function domRemoveNode(node) { } /** - * Creates a new text node in the given document. + * Remove a child from the specified (parent) node. * - * @param {Document} doc Target document. - * @param {String} text Text composing new text node. - * @return {Text} Newly constructed text node. + * @param {Element} node Parent element. + * @param {Node} child Child node to remove. + * @return {Node} Removed node. */ -function domCreateTextNode(doc, text) { - return doc.createTextNode(text); +function domRemoveChild(node, child) { + return node.removeChild(child); } + /** - * Creates a new node in the given document + * Trim whitespace from begin and end of string. * - * @param {Document} doc Target document. - * @param {String} name Name of new element (i.e. the tag name).. - * @return {Element} Newly constructed element. + * @see testStringTrim(); + * + * @param {string} str Input string. + * @return {string} Trimmed string. */ -function domCreateElement(doc, name) { - return doc.createElement(name); +function stringTrim(str) { + return stringTrimRight(stringTrimLeft(str)); } /** - * Creates a new attribute in the given document. + * Trim whitespace from beginning of string. * - * @param {Document} doc Target document. - * @param {String} name Name of new attribute. - * @return {Attr} Newly constructed attribute. + * @see testStringTrimLeft(); + * + * @param {string} str Input string. + * @return {string} Trimmed string. */ -function domCreateAttribute(doc, name) { - return doc.createAttribute(name); +function stringTrimLeft(str) { + return str.replace(/^\s+/, ""); } /** - * Creates a new comment in the given document. + * Trim whitespace from end of string. * - * @param {Document} doc Target document. - * @param {String} text Comment text. - * @return {Comment} Newly constructed comment. - */ -function domCreateComment(doc, text) { - return doc.createComment(text); + * @see testStringTrimRight(); + * + * @param {string} str Input string. + * @return {string} Trimmed string. + */ +function stringTrimRight(str) { + return str.replace(/\s+$/, ""); } - /** - * Creates a document fragment. + * Author: Steffen Meschkat <mesch@google.com> * - * @param {Document} doc Target document. - * @return {DocumentFragment} Resulting document fragment node. + * @fileoverview This class is used to evaluate expressions in a local + * context. Used by JstProcessor. */ -function domCreateDocumentFragment(doc) { - return doc.createDocumentFragment(); -} + /** - * Redirect to document.getElementById - * - * @param {Document} doc Target document. - * @param {String} id Id of requested node. - * @return {Element|Null} Resulting element. + * 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. */ -function domGetElementById(doc, id) { - return doc.getElementById(id); +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); } /** - * Redirect to window.setInterval + * 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 {Window} win Target window. - * @param {Function} fun Callback function. - * @param {Number} time Time in milliseconds. - * @return {Object} Contract id. + * @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. */ -function windowSetInterval(win, fun, time) { - return win.setInterval(fun, time); -} +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) { + copyProperties(me.vars_, opt_parent.vars_); + } else { + 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) { + 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_ = {} + /** - * Redirect to window.clearInterval + * 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) * - * @param {Window} win Target window. - * @param {object} id Contract id. - * @return {any} NOTE: Return type unknown? + * @type Array.<JsEvalContext> */ -function windowClearInterval(win, id) { - return win.clearInterval(id); -} +JsEvalContext.recycledInstances_ = []; + /** - * Determines whether one node is recursively contained in another. - * @param parent The parent node. - * @param child The node to look for in parent. - * @return parent recursively contains child + * A factory to create a JsEvalContext instance, possibly reusing + * one from recycledInstances_. (IE6 perf) + * + * @param {Object} opt_data + * @param {JsEvalContext} opt_parent + * @return {JsEvalContext} */ -function containsNode(parent, child) { - while (parent != child && child.parentNode) { - child = child.parentNode; +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); } - return parent == child; }; + + /** - * @fileoverview This file contains javascript utility functions that - * do not depend on anything defined elsewhere. + * 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_) { + delete instance.vars_[i]; + } + instance.data_ = null; + JsEvalContext.recycledInstances_.push(instance); +}; + /** - * Returns the value of the length property of the given object. Used - * to reduce compiled code size. + * 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. * - * @param {Array | String} a The string or array to interrogate. - * @return {Number} The value of the length property. + * @return {Object|null} The value of the expression from which + * exprFunction was created in the current js expression context and + * the context of template. */ -function jsLength(a) { - return a.length; -} +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]; + } +}; -var min = Math.min; -var max = Math.max; -var ceil = Math.ceil; -var floor = Math.floor; -var round = Math.round; -var abs = Math.abs; /** - * Copies all properties from second object to the first. Modifies to. + * 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} to The target object. - * @param {Object} from The source object. + * @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} */ -function copyProperties(to, from) { - foreachin(from, function(p) { - to[p] = from[p]; - }); -} +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; +}; + /** - * Iterates over the array, calling the given function for each - * element. + * 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 {Array} array - * @param {Function} fn + * @param {string} name + * + * @param {Object?} value */ -function foreach(array, fn) { - var I = jsLength(array); - for (var i = 0; i < I; ++i) { - fn(array[i], i); - } -} +JsEvalContext.prototype.setVariable = function(name, value) { + this.vars_[name] = value; +}; + /** - * Safely iterates over all properties of the given object, calling - * the given function for each property. If opt_all isn't true, uses - * hasOwnProperty() to assure the property is on the object, not on - * its prototype. + * 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 * - * @param {Object} object - * @param {Function} fn - * @param {Boolean} opt_all If true, also iterates over inherited properties. + * @return {Object?} value */ -function foreachin(object, fn, opt_all) { - for (var i in object) { - if (opt_all || !object.hasOwnProperty || object.hasOwnProperty(i)) { - fn(i, object[i]); - } - } -} +JsEvalContext.prototype.getVariable = function(name) { + return this.vars_[name]; +}; + /** - * Appends the second array to the first, copying its elements. - * Optionally only a slice of the second array is copied. + * 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" * - * @param {Array} a1 Target array (modified). - * @param {Array} a2 Source array. - * @param {Number} opt_begin Begin of slice of second array (optional). - * @param {Number} opt_end End (exclusive) of slice of second array (optional). + * @return {Object?} value */ -function arrayAppend(a1, a2, opt_begin, opt_end) { - var i0 = opt_begin || 0; - var i1 = opt_end || jsLength(a2); - for (var i = i0; i < i1; ++i) { - a1.push(a2[i]); - } -} +JsEvalContext.prototype.evalExpression = function(expr, opt_template) { + var exprFunction = jsEvalToFunction(expr); + return this.jsexec(exprFunction, opt_template); +}; + /** - * Trim whitespace from begin and end of string. + * 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. * - * @see testStringTrim(); + * @param {string} expr A javascript expression. * - * @param {String} str Input string. - * @return {String} Trimmed string. + * @return {Function} A function that returns the value of expr in the + * context of vars and data. */ -function stringTrim(str) { - return stringTrimRight(stringTrimLeft(str)); +function jsEvalToFunction(expr) { + if (!JsEvalContext.evalToFunctionCache_[expr]) { + try { + JsEvalContext.evalToFunctionCache_[expr] = + new Function(STRING_a, STRING_b, STRING_with + expr); + } catch (e) { + log('jsEvalToFunction (' + expr + ') EXCEPTION ' + e); + } + } + return JsEvalContext.evalToFunctionCache_[expr]; } + /** - * Trim whitespace from beginning of string. + * Evaluates the given expression to itself. This is meant to pass + * through string attribute values. * - * @see testStringTrimLeft(); + * @param {string} expr * - * @param {String} str Input string. - * @return {String} Trimmed string. + * @return {string} */ -function stringTrimLeft(str) { - return str.replace(/^\s+/, ""); +function jsEvalToSelf(expr) { + return expr; } + /** - * Trim whitespace from end of string. + * 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. * - * @see testStringTrimRight(); + * @param {string} expr * - * @param {String} str Input string. - * @return {String} Trimmed string. + * @return {Array} */ -function stringTrimRight(str) { - return str.replace(/\s+$/, ""); +function jsEvalToValues(expr) { + 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; } + /** - * Jscompiler wrapper for parseInt() with base 10. + * 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} s String repersentation of a number. + * @param {string} expr * - * @return {Number} The integer contained in s, converted on base 10. + * @return {Array.<Function>} */ -function parseInt10(s) { - return parseInt(s, 10); +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; } /** + * Author: Steffen Meschkat <mesch@google.com> + * * @fileoverview A simple formatter to project JavaScript data into * HTML templates. The template is edited in place. I.e. in order to * instantiate a template, clone it from the DOM first, and then @@ -646,9 +817,9 @@ function parseInt10(s) { * If the templates is processed again, changed values are merely * updated. * - * NOTE: IE DOM doesn't have importNode(). + * NOTE(mesch): IE DOM doesn't have importNode(). * - * NOTE: The property name "length" must not be used in input + * NOTE(mesch): The property name "length" must not be used in input * data, see comment in jstSelect_(). */ @@ -662,334 +833,512 @@ var ATT_select = 'jsselect'; var ATT_instance = 'jsinstance'; var ATT_display = 'jsdisplay'; var ATT_values = 'jsvalues'; +var ATT_vars = 'jsvars'; var ATT_eval = 'jseval'; var ATT_transclude = 'transclude'; var ATT_content = 'jscontent'; +var ATT_skip = 'jsskip'; /** - * Names of special variables defined by the jstemplate evaluation - * context. These can be used in js expression in jstemplate - * attributes. + * Name of the attribute that caches a reference to the parsed + * template processing attribute values on a template node. */ -var VAR_index = '$index'; -var VAR_this = '$this'; +var ATT_jstcache = 'jstcache'; /** - * 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. + * Name of the property that caches the parsed template processing + * attribute values on a template node. + */ +var PROP_jstcache = '__jstcache'; + + +/** + * ID of the element that contains dynamically loaded jstemplates. + */ +var STRING_jsts = 'jsts'; + + +/** + * Un-inlined string literals, to avoid object creation in + * IE6. + */ +var CHAR_asterisk = '*'; +var CHAR_dollar = '$'; +var CHAR_period = '.'; +var CHAR_ampersand = '&'; +var STRING_div = 'div'; +var STRING_id = 'id'; +var STRING_asteriskzero = '*0'; +var STRING_zero = '0'; + + +/** + * HTML template processor. Data values are bound to HTML templates + * using the attributes transclude, jsselect, jsdisplay, jscontent, + * jsvalues. The template is modifed in place. The values of those + * attributes are JavaScript expressions that are evaluated in the + * context of the data object fragment. * - * @param {Object} opt_data The context object. Null if no context. + * @param {JsEvalContext} context Context created from the input data + * object. * - * @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. + * @param {Element} template DOM node of the template. This will be + * processed in place. After processing, it will still be a valid + * template that, if processed again with the same data, will remain + * unchanged. * - * @constructor + * @param {boolean} opt_debugging Optional flag to collect debugging + * information while processing the template. Only takes effect + * in MAPS_DEBUG. */ -function JsExprContext(opt_data, opt_parent) { - var me = this; +function jstProcess(context, template, opt_debugging) { + var processor = new JstProcessor; + JstProcessor.prepareTemplate_(template); /** - * 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 be a scalar - * value, including undefined. - * - * @type {Object} + * Caches the document of the template node, so we don't have to + * access it through ownerDocument. + * @type Document */ - me.data_ = opt_data; + processor.document_ = ownerDocument(template); - /** - * 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_. - * - * @type {Object} - */ - me.vars_ = {}; - if (opt_parent) { - copyProperties(me.vars_, opt_parent.vars_); - } - this.vars_[VAR_this] = me.data_; + processor.run_(bindFully(processor, processor.jstProcessOuter_, + context, template)); } /** - * Evaluates the given expression in the context of the current - * context object and the current local variables. - * - * @param {String} expr A javascript expression. - * - * @param {Element} template DOM node of the template. - * - * @return The value of that expression. - */ -JsExprContext.prototype.jseval = function(expr, template) { - with (this.vars_) { - with (this.data_) { - try { - return (function() { - return eval('[' + expr + '][0]'); - }).call(template); - } catch (e) { - return null; - } - } - } + * Internal class used by jstemplates to maintain context. This is + * necessary to process deep templates in Safari which has a + * relatively shallow maximum recursion depth of 100. + * @class + * @constructor + */ +function JstProcessor() { } /** - * 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().) - * - * @return {JsExprContext} + * Counter to generate node ids. These ids will be stored in + * ATT_jstcache and be used to lookup the preprocessed js attributes + * from the jstcache_. The id is stored in an attribute so it + * suvives cloneNode() and thus cloned template nodes can share the + * same cache entry. + * @type number */ -JsExprContext.prototype.clone = function(data, index) { - var ret = new JsExprContext(data, this); - ret.setVariable(VAR_index, index); - if (this.resolver_) { - ret.setSubTemplateResolver(this.resolver_); - } - return ret; -} +JstProcessor.jstid_ = 0; /** - * 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 + * Map from jstid to processed js attributes. + * @type Object */ -JsExprContext.prototype.setVariable = function(name, value) { - this.vars_[name] = value; -} +JstProcessor.jstcache_ = {}; + +/** + * The neutral cache entry. Used for all nodes that don't have any + * jst attributes. We still set the jsid attribute on those nodes so + * we can avoid to look again for all the other jst attributes that + * aren't there. Remember: not only the processing of the js + * attribute values is expensive and we thus want to cache it. The + * access to the attributes on the Node in the first place is + * expensive too. + */ +JstProcessor.jstcache_[0] = {}; /** - * Sets the function used to resolve the values of the transclude - * attribute into DOM nodes. By default, this is jstGetTemplate(). The - * value set here is inherited by clones of this context. - * - * @param {Function} resolver The function used to resolve transclude - * ids into a DOM node of a subtemplate. The DOM node returned by this - * function will be inserted into the template instance being - * processed. Thus, the resolver function must instantiate the - * subtemplate as necessary. + * Map from concatenated attribute string to jstid. + * The key is the concatenation of all jst atributes found on a node + * formatted as "name1=value1&name2=value2&...", in the order defined by + * JST_ATTRIBUTES. The value is the id of the jstcache_ entry that can + * be used for this node. This allows the reuse of cache entries in cases + * when a cached entry already exists for a given combination of attribute + * values. (For example when two different nodes in a template share the same + * JST attributes.) + * @type Object */ -JsExprContext.prototype.setSubTemplateResolver = function(resolver) { - this.resolver_ = resolver; -} +JstProcessor.jstcacheattributes_ = {}; /** - * Resolves a sub template from an id. Used to process the transclude - * attribute. If a resolver function was set using - * setSubTemplateResolver(), it will be used, otherwise - * jstGetTemplate(). - * - * @param {String} id The id of the sub template. - * - * @return {Node} The root DOM node of the sub template, for direct - * insertion into the currently processed template instance. + * Map for storing temporary attribute values in prepareNode_() so they don't + * have to be retrieved twice. (IE6 perf) + * @type Object */ -JsExprContext.prototype.getSubTemplate = function(id) { - return (this.resolver_ || jstGetTemplate).call(this, id); -} +JstProcessor.attributeValues_ = {}; /** - * HTML template processor. Data values are bound to HTML templates - * using the attributes transclude, jsselect, jsdisplay, jscontent, - * jsvalues. The template is modifed in place. The values of those - * attributes are JavaScript expressions that are evaluated in the - * context of the data object fragment. + * A list for storing non-empty attributes found on a node in prepareNode_(). + * The array is global since it can be reused - this way there is no need to + * construct a new array object for each invocation. (IE6 perf) + * @type Array + */ +JstProcessor.attributeList_ = []; + + +/** + * Prepares the template: preprocesses all jstemplate attributes. * - * @param {JsExprContext} context Context created from the input data - * object. + * @param {Element} template + */ +JstProcessor.prepareTemplate_ = function(template) { + if (!template[PROP_jstcache]) { + domTraverseElements(template, function(node) { + JstProcessor.prepareNode_(node); + }); + } +}; + + +/** + * A list of attributes we use to specify jst processing instructions, + * and the functions used to parse their values. * - * @param {Element} template DOM node of the template. This will be - * processed in place. After processing, it will still be a valid - * template that, if processed again with the same data, will remain - * unchanged. + * @type Array.<Array> */ -function jstProcess(context, template) { - var processor = new JstProcessor(); - processor.run_([ processor, processor.jstProcess_, context, template ]); -} +var JST_ATTRIBUTES = [ + [ ATT_select, jsEvalToFunction ], + [ ATT_display, jsEvalToFunction ], + [ ATT_values, jsEvalToValues ], + [ ATT_vars, jsEvalToValues ], + [ ATT_eval, jsEvalToExpressions ], + [ ATT_transclude, jsEvalToSelf ], + [ ATT_content, jsEvalToFunction ], + [ ATT_skip, jsEvalToFunction ] +]; /** - * Internal class used by jstemplates to maintain context. - * NOTE: This is necessary to process deep templates in Safari - * which has a relatively shallow stack. - * @class + * Prepares a single node: preprocesses all template attributes of the + * node, and if there are any, assigns a jsid attribute and stores the + * preprocessed attributes under the jsid in the jstcache. + * + * @param {Element} node + * + * @return {Object} The jstcache entry. The processed jst attributes + * are properties of this object. If the node has no jst attributes, + * returns an object with no properties (the jscache_[0] entry). */ -function JstProcessor() { -} +JstProcessor.prepareNode_ = function(node) { + if (node[PROP_jstcache]) { + return node[PROP_jstcache]; + } + + + var jstid = domGetAttribute(node, ATT_jstcache); + if (jstid != null) { + return node[PROP_jstcache] = JstProcessor.jstcache_[jstid]; + } + + var attributeValues = JstProcessor.attributeValues_; + var attributeList = JstProcessor.attributeList_; + attributeList.length = 0; + + for (var i = 0, I = jsLength(JST_ATTRIBUTES); i < I; ++i) { + var name = JST_ATTRIBUTES[i][0]; + var value = domGetAttribute(node, name); + attributeValues[name] = value; + if (value != null) { + attributeList.push(name + "=" + value); + } + } + + if (attributeList.length == 0) { + domSetAttribute(node, ATT_jstcache, STRING_zero); + return node[PROP_jstcache] = JstProcessor.jstcache_[0]; + } + + var attstring = attributeList.join(CHAR_ampersand); + if (jstid = JstProcessor.jstcacheattributes_[attstring]) { + domSetAttribute(node, ATT_jstcache, jstid); + return node[PROP_jstcache] = JstProcessor.jstcache_[jstid]; + } + + var jstcache = {}; + for (var i = 0, I = jsLength(JST_ATTRIBUTES); i < I; ++i) { + var att = JST_ATTRIBUTES[i]; + var name = att[0]; + var parse = att[1]; + var value = attributeValues[name]; + if (value != null) { + jstcache[name] = parse(value); + } + } + + jstid = STRING_empty + ++JstProcessor.jstid_; + domSetAttribute(node, ATT_jstcache, jstid); + JstProcessor.jstcache_[jstid] = jstcache; + JstProcessor.jstcacheattributes_[attstring] = jstid; + + return node[PROP_jstcache] = jstcache; +}; /** - * Runs the state machine, beginning with function "start". + * Runs the given function in our state machine. * - * @param {Array} start The first function to run, in the form - * [object, method, args ...] + * It's informative to view the set of all function calls as a tree: + * - nodes are states + * - edges are state transitions, implemented as calls to the pending + * functions in the stack. + * - pre-order function calls are downward edges (recursion into call). + * - post-order function calls are upward edges (return from call). + * - leaves are nodes which do not recurse. + * We represent the call tree as an array of array of calls, indexed as + * stack[depth][index]. Here [depth] indexes into the call stack, and + * [index] indexes into the call queue at that depth. We require a call + * queue so that a node may branch to more than one child + * (which will be called serially), typically due to a loop structure. + * + * @param {Function} f The first function to run. */ -JstProcessor.prototype.run_ = function(start) { +JstProcessor.prototype.run_ = function(f) { var me = this; - me.queue_ = [ start ]; - while (jsLength(me.queue_)) { - var f = me.queue_.shift(); - f[1].apply(f[0], f.slice(2)); + /** + * A stack of queues of pre-order calls. + * The inner arrays (constituent queues) are structured as + * [ arg2, arg1, method, arg2, arg1, method, ...] + * ie. a flattened array of methods with 2 arguments, in reverse order + * for efficient push/pop. + * + * The outer array is a stack of such queues. + * + * @type Array.<Array> + */ + var calls = me.calls_ = []; + + /** + * The index into the queue for each depth. NOTE: Alternative would + * be to maintain the queues in reverse order (popping off of the + * end) but the repeated calls to .pop() consumed 90% of this + * function's execution time. + * @type Array.<number> + */ + var queueIndices = me.queueIndices_ = []; + + /** + * A pool of empty arrays. Minimizes object allocation for IE6's benefit. + * @type Array.<Array> + */ + var arrayPool = me.arrayPool_ = []; + + f(); + var queue, queueIndex; + var method, arg1, arg2; + var temp; + while (calls.length) { + queue = calls[calls.length - 1]; + queueIndex = queueIndices[queueIndices.length - 1]; + if (queueIndex >= queue.length) { + me.recycleArray_(calls.pop()); + queueIndices.pop(); + continue; + } + + method = queue[queueIndex++]; + arg1 = queue[queueIndex++]; + arg2 = queue[queueIndex++]; + queueIndices[queueIndices.length - 1] = queueIndex; + method.call(me, arg1, arg2); } -} +}; /** - * Appends a function to be called later. - * Analogous to calling that function on a subsequent line, or a subsequent - * iteration of a loop. + * Pushes one or more functions onto the stack. These will be run in sequence, + * interspersed with any recursive calls that they make. + * + * This method takes ownership of the given array! * - * @param {Array} f A function in the form [object, method, args ...] + * @param {Array} args Array of method calls structured as + * [ method, arg1, arg2, method, arg1, arg2, ... ] */ -JstProcessor.prototype.enqueue_ = function(f) { - this.queue_.push(f); -} +JstProcessor.prototype.push_ = function(args) { + this.calls_.push(args); + this.queueIndices_.push(0); +}; /** - * Implements internals of jstProcess. + * Enable/disable debugging. + * @param {boolean} debugging New state + */ +JstProcessor.prototype.setDebugging = function(debugging) { +}; + + +JstProcessor.prototype.createArray_ = function() { + if (this.arrayPool_.length) { + return this.arrayPool_.pop(); + } else { + return []; + } +}; + + +JstProcessor.prototype.recycleArray_ = function(array) { + arrayClear(array); + this.arrayPool_.push(array); +}; + +/** + * Implements internals of jstProcess. This processes the two + * attributes transclude and jsselect, which replace or multiply + * elements, hence the name "outer". The remainder of the attributes + * is processed in jstProcessInner_(), below. That function + * jsProcessInner_() only processes attributes that affect an existing + * node, but doesn't create or destroy nodes, hence the name + * "inner". jstProcessInner_() is called through jstSelect_() if there + * is a jsselect attribute (possibly for newly created clones of the + * current template node), or directly from here if there is none. * - * @param {JsExprContext} context + * @param {JsEvalContext} context * * @param {Element} template */ -JstProcessor.prototype.jstProcess_ = function(context, template) { +JstProcessor.prototype.jstProcessOuter_ = function(context, template) { var me = this; - var transclude = domGetAttribute(template, ATT_transclude); + var jstAttributes = me.jstAttributes_(template); + + var transclude = jstAttributes[ATT_transclude]; if (transclude) { - var tr = context.getSubTemplate(transclude); + var tr = jstGetTemplate(transclude); if (tr) { domReplaceChild(tr, template); - me.enqueue_([ me, me.jstProcess_, context, tr ]); + var call = me.createArray_(); + call.push(me.jstProcessOuter_, context, tr); + me.push_(call); } else { domRemoveNode(template); } return; } - var select = domGetAttribute(template, ATT_select); + var select = jstAttributes[ATT_select]; if (select) { me.jstSelect_(context, template, select); - return; + } else { + me.jstProcessInner_(context, template); } +}; + + +/** + * Implements internals of jstProcess. This processes all attributes + * except transclude and jsselect. It is called either from + * jstSelect_() for nodes that have a jsselect attribute so that the + * jsselect attribute will not be processed again, or else directly + * from jstProcessOuter_(). See the comment on jstProcessOuter_() for + * an explanation of the name. + * + * @param {JsEvalContext} context + * + * @param {Element} template + */ +JstProcessor.prototype.jstProcessInner_ = function(context, template) { + var me = this; + + var jstAttributes = me.jstAttributes_(template); - var display = domGetAttribute(template, ATT_display); + var display = jstAttributes[ATT_display]; if (display) { - if (!context.jseval(display, template)) { + var shouldDisplay = context.jsexec(display, template); + if (!shouldDisplay) { displayNone(template); return; } - displayDefault(template); } + var values = jstAttributes[ATT_vars]; + if (values) { + me.jstVars_(context, template, values); + } - var values = domGetAttribute(template, ATT_values); + values = jstAttributes[ATT_values]; if (values) { me.jstValues_(context, template, values); } - var expressions = domGetAttribute(template, ATT_eval); + var expressions = jstAttributes[ATT_eval]; if (expressions) { - foreach(expressions.split(/\s*;\s*/), function(expression) { - expression = stringTrim(expression); - if (jsLength(expression)) { - context.jseval(expression, template); - } - }); + for (var i = 0, I = jsLength(expressions); i < I; ++i) { + context.jsexec(expressions[i], template); + } + } + + var skip = jstAttributes[ATT_skip]; + if (skip) { + var shouldSkip = context.jsexec(skip, template); + if (shouldSkip) return; } - var content = domGetAttribute(template, ATT_content); + var content = jstAttributes[ATT_content]; if (content) { me.jstContent_(context, template, content); } else { - var childnodes = []; - for (var i = 0; i < jsLength(template.childNodes); ++i) { - if (template.childNodes[i].nodeType == DOM_ELEMENT_NODE) { - me.enqueue_( - [ me, me.jstProcess_, context, template.childNodes[i] ]); + var queue = me.createArray_(); + for (var c = template.firstChild; c; c = c.nextSibling) { + if (c.nodeType == DOM_ELEMENT_NODE) { + queue.push(me.jstProcessOuter_, context, c); } } + if (queue.length) me.push_(queue); } -} +}; /** * Implements the jsselect attribute: evalutes the value of the * jsselect attribute in the current context, with the current - * variable bindings (see JsExprContext.jseval()). If the value is an + * variable bindings (see JsEvalContext.jseval()). If the value is an * array, the current template node is multiplied once for every * element in the array, with the array element being the context * object. If the array is empty, or the value is undefined, then the * current template node is dropped. If the value is not an array, * then it is just made the context object. * - * @param {JsExprContext} context The current evaluation context. + * @param {JsEvalContext} context The current evaluation context. * * @param {Element} template The currently processed node of the template. * - * @param {String} select The javascript expression to evaluate. + * @param {Function} select The javascript expression to evaluate. * - * @param {Function} process The function to continue processing with. + * @notypecheck FIXME(hmitchell): See OCL6434950. instance and value need + * type checks. */ JstProcessor.prototype.jstSelect_ = function(context, template, select) { var me = this; - var value = context.jseval(select, template); - domRemoveAttribute(template, ATT_select); + var value = context.jsexec(select, template); var instance = domGetAttribute(template, ATT_instance); - var instance_last = false; + + var instanceLast = false; if (instance) { - if (instance.charAt(0) == '*') { + if (instance.charAt(0) == CHAR_asterisk) { instance = parseInt10(instance.substr(1)); - instance_last = true; + instanceLast = true; } else { - instance = parseInt10(instance); + instance = parseInt10(/** @type string */(instance)); } } - var multiple = (value !== null && - typeof value == 'object' && - typeof value.length == 'number'); - var multiple_empty = (multiple && value.length == 0); + var multiple = isArray(value); + var count = multiple ? jsLength(value) : 1; + var multipleEmpty = (multiple && count == 0); if (multiple) { - if (multiple_empty) { + if (multipleEmpty) { if (!instance) { - domSetAttribute(template, ATT_select, select); - domSetAttribute(template, ATT_instance, '*0'); + domSetAttribute(template, ATT_instance, STRING_asteriskzero); displayNone(template); } else { domRemoveNode(template); @@ -997,82 +1346,79 @@ JstProcessor.prototype.jstSelect_ = function(context, template, select) { } else { displayDefault(template); - if (instance === null || instance === "" || instance === undefined || - (instance_last && instance < jsLength(value) - 1)) { - var templatenodes = []; - var instances_start = instance || 0; - for (var i = instances_start + 1; i < jsLength(value); ++i) { + if (instance === null || instance === STRING_empty || + (instanceLast && instance < count - 1)) { + var queue = me.createArray_(); + + var instancesStart = instance || 0; + var i, I, clone; + for (i = instancesStart, I = count - 1; i < I; ++i) { var node = domCloneNode(template); - templatenodes.push(node); domInsertBefore(node, template); - } - templatenodes.push(template); - for (var i = 0; i < jsLength(templatenodes); ++i) { - var ii = i + instances_start; - var v = value[ii]; - var t = templatenodes[i]; + jstSetInstance(/** @type Element */(node), value, i); + clone = context.clone(value[i], i, count); - me.enqueue_([ me, me.jstProcess_, context.clone(v, ii), t ]); - var instanceStr = (ii == jsLength(value) - 1 ? '*' : '') + ii; - me.enqueue_( - [ null, postProcessMultiple_, t, select, instanceStr ]); - } + queue.push(me.jstProcessInner_, clone, node, + JsEvalContext.recycle, clone, null); - } else if (instance < jsLength(value)) { + } + jstSetInstance(template, value, i); + clone = context.clone(value[i], i, count); + queue.push(me.jstProcessInner_, clone, template, + JsEvalContext.recycle, clone, null); + me.push_(queue); + } else if (instance < count) { var v = value[instance]; - me.enqueue_( - [me, me.jstProcess_, context.clone(v, instance), template]); - var instanceStr = (instance == jsLength(value) - 1 ? '*' : '') - + instance; - me.enqueue_( - [ null, postProcessMultiple_, template, select, instanceStr ]); + jstSetInstance(template, value, instance); + var clone = context.clone(v, instance, count); + var queue = me.createArray_(); + queue.push(me.jstProcessInner_, clone, template, + JsEvalContext.recycle, clone, null); + me.push_(queue); } else { domRemoveNode(template); } } } else { if (value == null) { - domSetAttribute(template, ATT_select, select); displayNone(template); } else { - me.enqueue_( - [ me, me.jstProcess_, context.clone(value, 0), template ]); - me.enqueue_( - [ null, postProcessSingle_, template, select ]); + displayDefault(template); + var clone = context.clone(value, 0, 1); + var queue = me.createArray_(); + queue.push(me.jstProcessInner_, clone, template, + JsEvalContext.recycle, clone, null); + me.push_(queue); } } -} +}; /** - * Sets ATT_select and ATT_instance following recursion to jstProcess. + * Implements the jsvars attribute: evaluates each of the values and + * assigns them to variables in the current context. Similar to + * jsvalues, except that all values are treated as vars, independent + * of their names. * - * @param {Element} template The template + * @param {JsEvalContext} context Current evaluation context. * - * @param {String} select The jsselect string - * - * @param {String} instanceStr The new value for the jsinstance attribute - */ -function postProcessMultiple_(template, select, instanceStr) { - domSetAttribute(template, ATT_select, select); - domSetAttribute(template, ATT_instance, instanceStr); -} - - -/** - * Sets ATT_select and makes the element visible following recursion to - * jstProcess. - * - * @param {Element} template The template + * @param {Element} template Currently processed template node. * - * @param {String} select The jsselect string - */ -function postProcessSingle_(template, select) { - domSetAttribute(template, ATT_select, select); - displayDefault(template); -} + * @param {Array} values Processed value of the jsvalues attribute: a + * flattened array of pairs. The second element in the pair is a + * function that can be passed to jsexec() for evaluation in the + * current jscontext, and the first element is the variable name that + * the value returned by jsexec is assigned to. + */ +JstProcessor.prototype.jstVars_ = function(context, template, values) { + for (var i = 0, I = jsLength(values); i < I; i += 2) { + var label = values[i]; + var value = context.jsexec(values[i+1], template); + context.setVariable(label, value); + } +}; /** @@ -1084,28 +1430,26 @@ function postProcessSingle_(template, select) { * strings, the value is coerced to string in the latter case, * otherwise it's the uncoerced javascript value. * - * @param {JsExprContext} context Current evaluation context. + * @param {JsEvalContext} context Current evaluation context. * * @param {Element} template Currently processed template node. * - * @param {String} valuesStr Value of the jsvalues attribute to be - * processed. + * @param {Array} values Processed value of the jsvalues attribute: a + * flattened array of pairs. The second element in the pair is a + * function that can be passed to jsexec() for evaluation in the + * current jscontext, and the first element is the label that + * determines where the value returned by jsexec is assigned to. */ -JstProcessor.prototype.jstValues_ = function(context, template, valuesStr) { - var values = valuesStr.split(/\s*;\s*/); - for (var i = 0; i < jsLength(values); ++i) { - var colon = values[i].indexOf(':'); - if (colon < 0) { - continue; - } - var label = stringTrim(values[i].substr(0, colon)); - var value = context.jseval(values[i].substr(colon + 1), template); +JstProcessor.prototype.jstValues_ = function(context, template, values) { + for (var i = 0, I = jsLength(values); i < I; i += 2) { + var label = values[i]; + var value = context.jsexec(values[i+1], template); - if (label.charAt(0) == '$') { + if (label.charAt(0) == CHAR_dollar) { context.setVariable(label, value); - } else if (label.charAt(0) == '.') { - var nameSpaceLabel = label.substr(1).split('.'); + } else if (label.charAt(0) == CHAR_period) { + var nameSpaceLabel = label.substr(1).split(CHAR_period); var nameSpaceObject = template; var nameSpaceDepth = jsLength(nameSpaceLabel); for (var j = 0, J = nameSpaceDepth - 1; j < J; ++j) { @@ -1116,19 +1460,20 @@ JstProcessor.prototype.jstValues_ = function(context, template, valuesStr) { nameSpaceObject = nameSpaceObject[jLabel]; } nameSpaceObject[nameSpaceLabel[nameSpaceDepth - 1]] = value; + } else if (label) { - if (typeof value == 'boolean') { + if (typeof value == TYPE_boolean) { if (value) { domSetAttribute(template, label, label); } else { domRemoveAttribute(template, label); } } else { - domSetAttribute(template, label, '' + value); + domSetAttribute(template, label, STRING_empty + value); } } } -} +}; /** @@ -1137,46 +1482,201 @@ JstProcessor.prototype.jstValues_ = function(context, template, valuesStr) { * and assigns its string value to the content of the current template * node. * - * @param {JsExprContext} context Current evaluation context. + * @param {JsEvalContext} context Current evaluation context. * * @param {Element} template Currently processed template node. * - * @param {String} content Value of the jscontent attribute to be - * processed. + * @param {Function} content Processed value of the jscontent + * attribute. */ JstProcessor.prototype.jstContent_ = function(context, template, content) { - var value = '' + context.jseval(content, template); + var value = STRING_empty + context.jsexec(content, template); if (template.innerHTML == value) { return; } while (template.firstChild) { domRemoveNode(template.firstChild); } - var t = domCreateTextNode(ownerDocument(template), value); + var t = domCreateTextNode(this.document_, value); domAppendChild(template, t); -} +}; + + +/** + * Caches access to and parsing of template processing attributes. If + * domGetAttribute() is called every time a template attribute value + * is used, it takes more than 10% of the time. + * + * @param {Element} template A DOM element node of the template. + * + * @return {Object} A javascript object that has all js template + * processing attribute values of the node as properties. + */ +JstProcessor.prototype.jstAttributes_ = function(template) { + if (template[PROP_jstcache]) { + return template[PROP_jstcache]; + } + + var jstid = domGetAttribute(template, ATT_jstcache); + if (jstid) { + return template[PROP_jstcache] = JstProcessor.jstcache_[jstid]; + } + + return JstProcessor.prepareNode_(template); +}; /** * Helps to implement the transclude attribute, and is the initial * call to get hold of a template from its ID. * - * @param {String} name The ID of the HTML element used as template. + * If the ID is not present in the DOM, and opt_loadHtmlFn is specified, this + * function will call that function and add the result to the DOM, before + * returning the template. * - * @returns {Element} The DOM node of the template. (Only element - * nodes can be found by ID, hence it's a Element.) + * @param {string} name The ID of the HTML element used as template. + * @param {Function} opt_loadHtmlFn A function which, when called, will return + * HTML that contains an element whose ID is 'name'. + * + * @return {Element|null} The DOM node of the template. (Only element nodes + * can be found by ID, hence it's a Element.) */ -function jstGetTemplate(name) { - var section = domGetElementById(document, name); +function jstGetTemplate(name, opt_loadHtmlFn) { + var doc = document; + var section; + if (opt_loadHtmlFn) { + section = jstLoadTemplateIfNotPresent(doc, name, opt_loadHtmlFn); + } else { + section = domGetElementById(doc, name); + } if (section) { - var ret = domCloneNode(section); - domRemoveAttribute(ret, 'id'); + JstProcessor.prepareTemplate_(section); + var ret = domCloneElement(section); + domRemoveAttribute(ret, STRING_id); return ret; } else { return null; } } +/** + * This function is the same as 'jstGetTemplate' but, if the template + * does not exist, throw an exception. + * + * @param {string} name The ID of the HTML element used as template. + * @param {Function} opt_loadHtmlFn A function which, when called, will return + * HTML that contains an element whose ID is 'name'. + * + * @return {Element} The DOM node of the template. (Only element nodes + * can be found by ID, hence it's a Element.) + */ +function jstGetTemplateOrDie(name, opt_loadHtmlFn) { + var x = jstGetTemplate(name, opt_loadHtmlFn); + check(x !== null); + return /** @type Element */(x); +} + + +/** + * If an element with id 'name' is not present in the document, call loadHtmlFn + * and insert the result into the DOM. + * + * @param {Document} doc + * @param {string} name + * @param {Function} loadHtmlFn A function that returns HTML to be inserted + * into the DOM. + * @param {string} opt_target The id of a DOM object under which to attach the + * HTML once it's inserted. An object with this id is created if it does not + * exist. + * @return {Element} The node whose id is 'name' + */ +function jstLoadTemplateIfNotPresent(doc, name, loadHtmlFn, opt_target) { + var section = domGetElementById(doc, name); + if (section) { + return section; + } + jstLoadTemplate_(doc, loadHtmlFn(), opt_target || STRING_jsts); + var section = domGetElementById(doc, name); + if (!section) { + log("Error: jstGetTemplate was provided with opt_loadHtmlFn, " + + "but that function did not provide the id '" + name + "'."); + } + return /** @type Element */(section); +} + + +/** + * Loads the given HTML text into the given document, so that + * jstGetTemplate can find it. + * + * We append it to the element identified by targetId, which is hidden. + * If it doesn't exist, it is created. + * + * @param {Document} doc The document to create the template in. + * + * @param {string} html HTML text to be inserted into the document. + * + * @param {string} targetId The id of a DOM object under which to attach the + * HTML once it's inserted. An object with this id is created if it does not + * exist. + */ +function jstLoadTemplate_(doc, html, targetId) { + var existing_target = domGetElementById(doc, targetId); + var target; + if (!existing_target) { + target = domCreateElement(doc, STRING_div); + target.id = targetId; + displayNone(target); + positionAbsolute(target); + domAppendChild(doc.body, target); + } else { + target = existing_target; + } + var div = domCreateElement(doc, STRING_div); + target.appendChild(div); + div.innerHTML = html; +} + + +/** + * Sets the jsinstance attribute on a node according to its context. + * + * @param {Element} template The template DOM node to set the instance + * attribute on. + * + * @param {Array} values The current input context, the array of + * values of which the template node will render one instance. + * + * @param {number} index The index of this template node in values. + */ +function jstSetInstance(template, values, index) { + if (index == jsLength(values) - 1) { + domSetAttribute(template, ATT_instance, CHAR_asterisk + index); + } else { + domSetAttribute(template, ATT_instance, STRING_empty + index); + } +} + + +/** + * Log the current state. + * @param {string} caller An identifier for the caller of .log_. + * @param {Element} template The template node being processed. + * @param {Object} jstAttributeValues The jst attributes of the template node. + */ +JstProcessor.prototype.logState_ = function( + caller, template, jstAttributeValues) { +}; + + +/** + * Retrieve the processing logs. + * @return {Array.<string>} The processing logs. + */ +JstProcessor.prototype.getLogs = function() { + return this.logs_; +}; window['jstGetTemplate'] = jstGetTemplate; +window['JsEvalContext'] = JsEvalContext; window['jstProcess'] = jstProcess; -window['JsExprContext'] = JsExprContext; +})() |