You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@shindig.apache.org by lr...@apache.org on 2008/09/11 00:21:55 UTC

svn commit: r694034 [2/2] - in /incubator/shindig/trunk/features: ./ opensocial-templates/

Added: incubator/shindig/trunk/features/opensocial-templates/domutil.js
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/features/opensocial-templates/domutil.js?rev=694034&view=auto
==============================================================================
--- incubator/shindig/trunk/features/opensocial-templates/domutil.js (added)
+++ incubator/shindig/trunk/features/opensocial-templates/domutil.js Wed Sep 10 15:21:55 2008
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+// A pseudo namespace.
+var domutil = {};
+
+/**
+ * Helper functions adapted from Selenium code.
+ *
+ * Determines if the specified element is visible. An element can be rendered
+ * invisible by setting the CSS "visibility" property to "hidden", or the
+ * "display" property to "none", either for the element itself or one if its
+ * ancestors. This method will fail if the element is not present.
+ * @param {Node} node The node to check.
+ * @return {boolean} Whether the node is visible.
+ */
+domutil.isVisible = function(node) {
+  // This test is necessary because getComputedStyle returns nothing
+  // for WebKit-based browsers if node not in document. See comment below.
+  if (node.style.display == 'none' || node.style.visibility == 'hidden') {
+    return false;
+  }
+  var visibility = this.findEffectiveStyleProperty(node, 'visibility');
+  var display = this.findEffectiveStyleProperty(node, 'display');
+  return visibility != 'hidden' && display != 'none';
+};
+
+
+/**
+ * Returns the value of the effective style specified by {@code property}.
+ * @param {Node} element The node to query.
+ * @param {string} property The name of a style that is of interest.
+ * @return {string} The value of style {@code property}.
+ */
+domutil.findEffectiveStyleProperty = function(element, property) {
+  var effectiveStyle = this.findEffectiveStyle(element);
+  var propertyValue = effectiveStyle[property];
+  if (propertyValue == 'inherit' && element.parentNode.style) {
+    return this.findEffectiveStyleProperty(element.parentNode, property);
+  }
+  return propertyValue;
+};
+
+
+/**
+ * Returns the effective style object.
+ * @param {Node} element The node to query.
+ * @return {CSSStyleDeclaration|undefined} The style object.
+ */
+domutil.findEffectiveStyle = function(element) {
+  if (!element.style) {
+    return undefined; // not a styled element
+  }
+  if (window.getComputedStyle) {
+    // DOM-Level-2-CSS
+    // WebKit-based browsers (Safari included) return nothing if the element
+    // is not a descendent of document ...
+    return window.getComputedStyle(element, null);
+  }
+  if (element.currentStyle) {
+    // non-standard IE alternative
+    return element.currentStyle;
+    // TODO: this won't really work in a general sense, as
+    //   currentStyle is not identical to getComputedStyle()
+    //   ... but it's good enough for 'visibility'
+  }
+  throw new Error('cannot determine effective stylesheet in this browser');
+};
+
+
+/**
+ * Returns the text content of the current node, without markup and invisible
+ * symbols. New lines are stripped and whitespace is collapsed,
+ * such that each character would be visible.
+ *
+ * @param {Node} node The node from which we are getting content.
+ * @return {string} The text content.
+ */
+domutil.getVisibleText = function(node) {
+  var textContent;
+  // NOTE(kjin): IE innerText is more like Firefox textContent -- visibility
+  // is not concerned. Safari 3 and Chrome innerText is just the visible text.
+  var buf = [];
+  domutil.getVisibleText_(node, buf, true);
+  textContent = buf.join('');
+
+  textContent = textContent.replace(/\xAD/g, '');
+
+  textContent = textContent.replace(/ +/g, ' ');
+  if (textContent != ' ') {
+    textContent = textContent.replace(/^\s*/, '');
+  }
+
+  return textContent;
+};
+
+
+/**
+ * Returns the domutil.getVisibleText without trailing space, if any.
+ *
+ * @param {Node} node The node from which we are getting content.
+ * @return {string} The text content.
+ */
+domutil.getVisibleTextTrim = function(node) {
+  return domutil.getVisibleText(node).replace(/^[\s\xa0]+|[\s\xa0]+$/g, '');
+};
+
+
+/**
+ * Recursive support function for text content retrieval.
+ *
+ * @param {Node} node The node from which we are getting content.
+ * @param {Array} buf string buffer.
+ * @param {boolean} normalizeWhitespace Whether to normalize whitespace.
+ * @private
+ */
+domutil.getVisibleText_ = function(node, buf, normalizeWhitespace) {
+  var TAGS_TO_IGNORE_ = {
+    'SCRIPT': 1,
+    'STYLE': 1,
+    'HEAD': 1,
+    'IFRAME': 1,
+    'OBJECT': 1
+  };
+  var PREDEFINED_TAG_VALUES_ = {'IMG': ' ', 'BR': '\n'};
+
+  if (node.nodeName in TAGS_TO_IGNORE_) {
+    // ignore certain tags
+  } else if (node.nodeType == 3) {
+    if (normalizeWhitespace) {
+      buf.push(String(node.nodeValue).replace(/(\r\n|\r|\n)/g, ''));
+    } else {
+      buf.push(node.nodeValue);
+    }
+  } else if (!domutil.isVisible(node)) {
+    // ignore invisible node
+    // this has to be after the check for NodeType.TEXT because text node
+    // does not have style.
+  } else if (node.nodeName in PREDEFINED_TAG_VALUES_) {
+    buf.push(PREDEFINED_TAG_VALUES_[node.nodeName]);
+  } else {
+    var child = node.firstChild;
+    while (child) {
+      domutil.getVisibleText_(child, buf, normalizeWhitespace);
+      child = child.nextSibling;
+    }
+  }
+};
+

Added: incubator/shindig/trunk/features/opensocial-templates/feature.xml
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/features/opensocial-templates/feature.xml?rev=694034&view=auto
==============================================================================
--- incubator/shindig/trunk/features/opensocial-templates/feature.xml (added)
+++ incubator/shindig/trunk/features/opensocial-templates/feature.xml Wed Sep 10 15:21:55 2008
@@ -0,0 +1,49 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations under the License.
+
+TODO(davidbyttow): In order to use this feature, you must specify
+tell the content-rewrite feature to allow tags. Otherwise,
+<script type="text/os-template"> will be parsed as JS.
+  E.g.,
+  <Optional feature="content-rewrite">
+    <Param name="include-tags"/>
+  </Optional>
+
+TODO(davidbyttow): Implement local versions of jstemplate.
+
+N.B.: Currently the jstemplate library is served directly from googlecode.
+This should not be used in a production setting.
+-->
+<feature>
+  <name>opensocial-templates</name>
+  <gadget>
+    <script src="http://google-jstemplate.googlecode.com/svn/trunk/util.js"></script>
+    <script src="http://google-jstemplate.googlecode.com/svn/trunk/jsevalcontext.js"></script>
+    <script src="http://google-jstemplate.googlecode.com/svn/trunk/jstemplate.js"></script>
+    <script src="jstemplate_debug.js"></script>
+    <script src="base.js"></script>
+    <script src="namespaces.js"></script>
+    <script src="util.js"></script>
+    <script src="template.js"></script>
+    <script src="compiler.js"></script>
+    <script src="loader.js"></script>
+    <script src="container.js"></script>
+    <script src="os.js"></script>
+    <script src="data.js"></script>
+  </gadget>
+</feature>

Added: incubator/shindig/trunk/features/opensocial-templates/jstemplate_debug.js
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/features/opensocial-templates/jstemplate_debug.js?rev=694034&view=auto
==============================================================================
--- incubator/shindig/trunk/features/opensocial-templates/jstemplate_debug.js (added)
+++ incubator/shindig/trunk/features/opensocial-templates/jstemplate_debug.js Wed Sep 10 15:21:55 2008
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+/**
+ * @fileoverview Debug functions to be linked against the JSTemplate library
+ * to provide debugging output.
+ * 
+ * TODO(levik): 
+ *  - Support more output types (currently Firebug only)
+ *  - Filter "mundane" errors (such as non-existent variable lookups) 
+ */
+
+
+function jsToSource(js) {} 
+
+function check(x) {}
+
+function logUrl(url) {}
+function logHtml(url) {}
+function logError(msg, file, line) {}
+function dump(exception) {}
+function rethrow(e) {}
+function getSourceLine(level) { return ""; }
+
+function Profiler() {}
+Profiler.monitor = function(obj, method, trace, opt_l, opt_fmt) {};
+Profiler.monitorAll = function(obj, trace, opt_l) {};
+Profiler.dump = function() {};

Added: incubator/shindig/trunk/features/opensocial-templates/loader.js
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/features/opensocial-templates/loader.js?rev=694034&view=auto
==============================================================================
--- incubator/shindig/trunk/features/opensocial-templates/loader.js (added)
+++ incubator/shindig/trunk/features/opensocial-templates/loader.js Wed Sep 10 15:21:55 2008
@@ -0,0 +1,284 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+/**
+ * @fileoverview OpenSocial Template loader. Can be used to load template
+ * libraries via URL. Supports Javascript and CSS injection.
+ *
+ * Usage: 
+ *   os.Loader.loadUrl("/path/templatelib.xml", function() { doSomething(); });
+ * 
+ * or
+ *   os.Loader.loadContent(
+ *       "<Templates><Template tag="foo:bar">...</Template></Templates>");
+ * 
+ * The Template Library should have the following structure:
+ *
+ *   <Templates xmlns:foo="http://foo.com/">
+ *     <Namspace prefix="foo" url="http://foo.com/"/>
+ *     <Template tag="foo:bar">[Template Markup Here]</Template>
+ *     <Style>[CSS for all templates]</Style>
+ *     <JavaScript>
+ *       function usedByAllTemplates() { ... };
+ *     </JavaScript>
+ *     <TemplateDef tag="foo:baz">
+ *       <Template>[Markup for foo:baz]</Template>
+ *       <Style>[CSS for foo:baz]</Style>
+ *       <JavaScript>
+ *         function usedByFooBaz() { ... };
+ *       </JavaScript>
+ *     </TemplateDef>
+ *   </Templates> 
+ * 
+ * TODO(levik): Implement dependency support - inject JS and CSS lazily.
+ * TODO(levik): More error handling and reporting of ill-formed XML files.
+ */
+  
+os.Loader = {};
+
+/**
+ * A map of URLs which were already loaded.
+ */
+os.Loader.loadedUrls_ = {};
+
+/**
+ * Load a remote URL via XMLHttpRequest or gadgets.io.makeRequest API
+ *     when in context of a gadget.
+ * @param {string} url The URL that is to be fetched.
+ * @param {Function} callback Function to call once loaded.
+ */
+os.Loader.loadUrl = function(url, callback) {
+  if (typeof(window['gadgets']) != "undefined") {
+    os.Loader.requestUrlGadgets_(url, callback);
+  } else {
+    os.Loader.requestUrlXHR_(url, callback);
+  }
+};
+
+/**
+ * Loads a Template Library from a URL via XMLHttpRequest. Once the library is
+ * loaded, the callback function is called. A map is kept to prevent loading
+ * the same URL twice.
+ * @param {string} url The URL of the Template Library.
+ * @param {Function} callback Function to call once loaded.
+ */
+os.Loader.requestUrlXHR_ = function(url, callback) {
+  if (os.Loader.loadedUrls_[url]) {
+    window.setTimeout(callback, 0);
+    return;
+  }
+  var req = null;
+  if (typeof(XMLHttpRequest) != "undefined") {
+    req = new XMLHttpRequest();
+  } else {
+    req = new ActiveXObject("MSXML2.XMLHTTP");
+  }
+  req.open("GET", url, true);
+  req.onreadystatechange = function() {
+    if (req.readyState == 4) {
+      os.Loader.loadContent(req.responseText);
+      os.Loader.loadedUrls_[url] = true;
+      callback();
+    }
+  };
+  req.send(null);
+}
+
+/**
+ * Fetch content remotely using the gadgets.io.makeRequest API.
+ * @param {string} url The URL where the content is located.
+ * @param {Function} callback Function to call with the data from the URL
+ *     once it is fetched.
+ */
+os.Loader.requestUrlGadgets_ = function(url, callback) {
+  var params = {};
+  var gadgets = window['gadgets'];
+
+  if (os.Loader.loadedUrls_[url]) {
+    window.setTimeout(callback, 0);
+    return;
+  }
+  params[gadgets.io.RequestParameters.CONTENT_TYPE] =
+      gadgets.io.ContentType.TEXT;
+  gadgets.io.makeRequest(url, function(obj) {
+    os.Loader.loadContent(obj.data);
+    os.Loader.loadedUrls_[url] = true;
+    callback();
+  }, params);
+};
+
+/**
+ * Loads a number of Template libraries, specified by an array of URLs. Once
+ * all the libraries have been loaded, the callback is called.
+ * @param {Array.<string>} urls An array of URLs of Template Libraries to load.
+ * @param {Function} callback Function to call once all libraries are loaded.
+ */
+os.Loader.loadUrls = function(urls, callback) {
+  var loadOne = function() {
+    if (urls.length == 0) {
+      callback();
+    } else {
+      os.Loader.loadUrl(urls.pop(), loadOne);
+    }
+  }
+  loadOne();
+};
+
+/**
+ * Processes the XML markup of a Template Library.
+ */
+os.Loader.loadContent = function(xmlString) {
+  var doc = os.parseXML_(xmlString);
+  var templatesNode = doc.firstChild;
+  os.Loader.processTemplatesNode(templatesNode);
+};
+
+/**
+ * Gets the function that should be used for processing a tag.
+ * @param {string} tagName Name of the tag.
+ * @return {Function|null} The function for processing such tags.
+ */
+os.Loader.getProcessorFunction_ = function(tagName) {
+  // TODO(levik): This won't work once compiler does name mangling.
+  return os.Loader['process' + tagName + 'Node'] || null;
+};
+
+/**
+ * Processes the <Templates> node.
+ */
+os.Loader.processTemplatesNode = function(node) {
+  for (var child = node.firstChild; child; child = child.nextSibling) {
+    if (child.nodeType == DOM_ELEMENT_NODE) {
+      var handler = os.Loader.getProcessorFunction_(child.tagName);
+      if (handler) {
+        handler(child);
+      }
+    }
+  }
+};
+
+/**
+ * Processes the <Namespace> node.
+ */
+os.Loader.processNamespaceNode = function(node) {
+  var prefix = node.getAttribute("prefix");
+  var url = node.getAttribute("url");
+  os.createNamespace(prefix, url);
+};
+
+/**
+ * Processes the <TemplateDef> node
+ */
+os.Loader.processTemplateDefNode = function(node) {
+  var tag = node.getAttribute("tag");
+  for (var child = node.firstChild; child; child = child.nextSibling) {
+    if (child.nodeType == DOM_ELEMENT_NODE) {
+      // TODO(levik): This won't work once compiler does name mangling.
+      var handler = os.Loader.getProcessorFunction_(child.tagName);
+      if (handler) {
+        handler(child, tag);
+      }
+    }
+  }
+};
+
+/**
+ * Processes the <Template> node
+ */
+os.Loader.processTemplateNode = function(node, opt_tag) {
+  var tag = opt_tag || node.getAttribute("tag");
+  if (tag) {
+    var tagParts = tag.split(":");
+    if (tagParts.length != 2) {
+      throw "Invalid tag name: " + tag; 
+    }
+    var nsObj = os.getNamespace(tagParts[0]);
+    if (!nsObj) {
+      throw "Namespace not registered: " + tagParts[0] + 
+          " while trying to define " + tag;
+    }
+    var template = os.compileXMLNode(node);
+    nsObj[tagParts[1]] = os.createTemplateCustomTag(template);
+  } 
+};
+
+/**
+ * Processes the <JavaScript> node
+ */
+os.Loader.processJavaScriptNode = function(node, opt_tag) {
+  for (var contentNode = node.firstChild; contentNode; 
+      contentNode = contentNode.nextSibling) {
+    // TODO(levik): Skip empty text nodes (with whitespace and newlines)
+    os.Loader.injectJavaScript(contentNode.nodeValue);
+  }
+};
+
+/**
+ * Processes the <Style> node
+ */
+os.Loader.processStyleNode = function(node, opt_tag) {
+  for (var contentNode = node.firstChild; contentNode; 
+      contentNode = contentNode.nextSibling) {
+    // TODO(levik): Skip empty text nodes (with whitespace and newlines)
+    os.Loader.injectStyle(contentNode.nodeValue);
+  }
+};
+
+/**
+ * @type {Element} DOM node used for dynamic injection of JavaScript.
+ * @private
+ * TODO(davidbyttow): Only retrieve this once if JavaScript injection was
+ * actually requested.
+ */
+os.Loader.headNode_ = document.getElementsByTagName('head')[0] ||
+    document.getElementsByTagName('*')[0];
+
+/**
+ * Injects and evaluates JavaScript code synchronously in the global scope.
+ */
+os.Loader.injectJavaScript = function(jsCode) {
+  var scriptNode = document.createElement('script');
+  scriptNode.type = 'text/javascript';
+  scriptNode.text = jsCode;
+  os.Loader.headNode_.appendChild(scriptNode);
+};
+
+/**
+ * Injects CSS Style code into the page.
+ */
+os.Loader.injectStyle = function(cssCode) {
+  var sheet;
+  if (document.styleSheets.length == 0) {
+    document.getElementsByTagName("head")[0].appendChild(
+        document.createElement("style"));
+  }
+  sheet = document.styleSheets[0];
+  var rules = cssCode.split("}");
+  for (var i = 0; i < rules.length; i++) {
+    var rule = rules[i].replace(/\n/g, "").replace(/\s+/g, " ");
+    if (rule.length > 2) {
+      if (sheet.insertRule) {
+        rule = rule + "}";
+        sheet.insertRule(rule, sheet.cssRules.length);
+      } else {
+        var ruleParts = rule.split("{");
+        sheet.addRule(ruleParts[0], ruleParts[1]);
+      }
+    }
+  }
+};

Added: incubator/shindig/trunk/features/opensocial-templates/loader_test.js
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/features/opensocial-templates/loader_test.js?rev=694034&view=auto
==============================================================================
--- incubator/shindig/trunk/features/opensocial-templates/loader_test.js (added)
+++ incubator/shindig/trunk/features/opensocial-templates/loader_test.js Wed Sep 10 15:21:55 2008
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+/**
+ * Unit test for injecting JavaScript into the global scope with the 
+ * os.Loader.
+ */
+function testInjectJavaScript() {
+  var jsCode = "function testFunction() { return 'foo'; }";
+  os.Loader.injectJavaScript(jsCode);
+  assertTrue(window.testFunction instanceof Function);
+  assertEquals(window.testFunction(), 'foo');
+}
+
+/**
+ * Unit test for injecting CSS through the os.Loader.
+ */
+function testInjectStyle() {
+  var cssCode = '.testCSS { width: 100px; height: 200px; }';
+  os.Loader.injectStyle(cssCode);
+  var rule = getStyleRule('.testCSS');
+  assertNotNull(rule);
+  assertEquals(rule.style.width, '100px');
+  assertEquals(rule.style.height, '200px');
+}
+
+/**
+ * @type {String} Template XML data for testLoadContent.
+ */
+var testContentXML =
+    '<Templates xmlns:test="http://www.google.com/#test">' +
+    '  <Namespace prefix="test" url="http://www.google.com/#test"/>' +
+    '  <Template tag="test:tag">' +
+    '    <div id="tag"></div>' +
+    '  </Template>' +
+    '  <JavaScript>' +
+    '    function testJavaScript() {' +
+    '      return "testJavaScript";' +
+    '    }' +
+    '  </JavaScript>' +
+    '  <Style>' +
+    '    .testStyle {' +
+    '      width: 24px;' +
+    '    }' +
+    '  </Style>' +
+    '  <TemplateDef tag="test:tagDef">' +
+    '    <Template>' +
+    '      <div id="tagDef"></div>' +
+    '    </Template>' +
+    '    <JavaScript>' +
+    '      function testJavaScriptDef() {' +
+    '        return "testJavaScriptDef";' +
+    '      }' +
+    '    </JavaScript>' +
+    '    <Style>' +
+    '      .testStyleDef {' +
+    '        height: 42px;' +
+    '      }' +
+    '    </Style>' +
+    '  </TemplateDef>' +
+    '</Templates>';
+
+/**
+ * System test for os.loadContent functionality. This tests
+ * all functionality except for XHR.
+ */
+function testLoadContent() {
+  os.Loader.loadContent(testContentXML);
+
+  // Verify registered tags.
+  var ns = os.nsmap_['test'];
+  assertNotNull(ns);
+  assertTrue(ns['tag'] instanceof Function);
+  assertTrue(ns['tagDef'] instanceof Function);
+
+  // Verify JavaScript functions.
+  assertTrue(window['testJavaScript'] instanceof Function);
+  assertEquals(window.testJavaScript(), 'testJavaScript');
+  assertTrue(window['testJavaScriptDef'] instanceof Function);
+  assertEquals(window.testJavaScriptDef(), 'testJavaScriptDef');
+
+  // Verify styles.
+  var rule = getStyleRule('.testStyle');
+  assertNotNull(rule);
+  assertEquals(rule.style.width, '24px');
+  var ruleDef = getStyleRule('.testStyleDef');
+  assertNotNull(ruleDef);
+  assertEquals(ruleDef.style.height, '42px');
+}
+
+/**
+ * Utility function for retrieving a style rule by selector text
+ * if its available.
+ * @param {string} name Selector text name.
+ * @return {Object} CSSRule object.
+ */
+function getStyleRule(name) {
+  var sheets = document.styleSheets;
+  for (var i = 0; i < sheets.length; ++i) {
+    var rules = sheets[i].cssRules || sheets[i].rules;
+    if (rules) {
+      for (var j = 0; j < rules.length; ++j) {
+        if (rules[j].selectorText == name) {
+          return rules[j];
+        }
+      }
+    }
+  }
+  return null;
+}

Added: incubator/shindig/trunk/features/opensocial-templates/namespaces.js
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/features/opensocial-templates/namespaces.js?rev=694034&view=auto
==============================================================================
--- incubator/shindig/trunk/features/opensocial-templates/namespaces.js (added)
+++ incubator/shindig/trunk/features/opensocial-templates/namespaces.js Wed Sep 10 15:21:55 2008
@@ -0,0 +1,203 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+/**
+ * @fileoverview Implements namespace support for custom tags.
+ *
+ * TODO(davidbyttow): Refactor this.
+ */
+
+/**
+ * Map of namespace collections.
+ *
+ * Each namespace collection is either a map of tag handlers, or an object
+ * that has a getTag(tagName) method that will return a tag handler based on 
+ * name.
+ * 
+ * A tag handler function should be have the following signature:
+ * function({Element} node, {Object} data, {JSEvalContext} context)
+ * where context is the JSEvalContext used to wrap data. 
+ *
+ * For simpler implementations,
+ * function({Element} node, {Object} data)
+ * can be used, omitting the third param.
+ * 
+ * Handler functions can return a string, a DOM Element or an Object with 
+ * {Element} root and, optionally, {Function} onAttach properties.
+ *
+ * @type {Object}
+ * @private  
+ */
+os.nsmap_ = {};
+
+/**
+ * Map of namespace prefixes to full urls.
+ * @type {Object}
+ * @private  
+ */
+os.nsurls_ = {};
+
+/***
+ * Registers the given namespace with a specified URL. Throws an error if it
+ * already exists as a different URL.
+ * @param {string} ns Namespace tag.
+ * @param {string} url URL for namespace.
+ * @return {Object} The object map of registered tags.
+ */
+os.createNamespace = function(ns, url) {
+  var tags = os.nsmap_[ns];
+  if (!tags) {
+    tags = {};
+    os.nsmap_[ns] = tags;
+    os.nsurls_[ns] = url;
+  } else if (os.nsurls_[ns] != url) {
+    throw("Namespace " + ns + " already defined with url " + os.nsurls_[ns]);
+  }
+  return tags;
+};
+
+/**
+ * Returns the namespace object for a given prefix.
+ */
+os.getNamespace = function(prefix) {
+  return os.nsmap_[prefix];
+};
+
+os.addNamespace = function(ns, url, nsObj) {
+  if (os.nsmap_[ns]) {
+    throw ("Namespace '" + ns + "' already exists!");
+  }
+  os.nsmap_[ns] = nsObj;
+  os.nsurls_[ns] = url;
+};
+
+os.getCustomTag = function(ns, tag) {
+  var nsObj = os.nsmap_[ns];
+  if (!nsObj) {
+    return null;
+  }
+  if (nsObj.getTag) {
+    return nsObj.getTag(tag);
+  } else {
+    return nsObj[tag];
+  }
+};
+
+/**
+ * Returns the XML namespace declarations that need to be injected into a
+ * particular template to make it valid XML. Uses the defined namespaces to
+ * see which are available, and checks that they are used in the supplied code.
+ * An empty string is returned if no injection is needed.
+ *
+ * @param {string} templateSrc Template source code.
+ * @return {string} A string of xmlns delcarations required for this tempalte.
+ */
+os.getRequiredNamespaces = function(templateSrc) {
+  var codeToInject = "";
+  for (var ns in os.nsurls_) {
+    if (templateSrc.indexOf("<" + ns + ":") >= 0 &&
+        templateSrc.indexOf("xmlns:" + ns + ":") < 0) {
+      codeToInject += " xmlns:" + ns + "=\"" + os.nsurls_[ns] + "\"";
+    }
+  }
+  return codeToInject;
+};
+
+/**
+ * Checks if an XML tag corresponds to a known custom function.
+ * If so, the namespace and tag name are returned in an array.
+ * @param {string} nodeName Name of XML tag.
+ * @return {Array.<string>|null}
+ */
+os.checkCustom_ = function(nodeName) {
+  var index;
+  if ((index = nodeName.indexOf(':')) < 0) {
+    return null;
+  }
+  var ns = nodeName.substring(0, index);
+  var tag = nodeName.substring(index + 1);
+  if (os.getCustomTag(ns, tag)) {
+    return [ns, tag]; 
+  }
+  return null;
+};
+
+/**
+ * Define 'os:renderAll' and 'os:Html' tags and the @onAttach attribute
+ */
+os.defineBuiltinTags = function() {
+  var osn = os.getNamespace("os") || 
+      os.createNamespace("os", "http://opensocial.com/#template");
+      
+  /**
+   * renderAll custom tag renders the specified child nodes of the current 
+   * context.
+   */
+  osn.renderAll = function(node, data, context) {
+    var parent = context.getVariable(os.VAR_parentnode);
+    var exp = node.getAttribute("content") || "*";
+    var result = os.getValueFromNode_(parent, exp);
+    if (!result) {
+       return ""; 
+    } else if (typeof(result) == "string") {
+      var textNode = document.createTextNode(result);
+      result = [];
+      result.push(textNode);
+    } else if (!isArray(result)) {
+      var resultArray = [];
+      for (var i = 0; i < result.childNodes.length; i++) {
+        resultArray.push(result.childNodes[i]);
+      }
+      result = resultArray;
+    } else if (exp != "*" && result.length == 1 &&
+        result[0].nodeType == DOM_ELEMENT_NODE) {
+      // When we call <os:renderAll content="tag"/>, render the inner content
+      // of the tag returned, not the tag itself.
+      var resultArray = [];
+      for (var i = 0; i < result[0].childNodes.length; i++) {
+        resultArray.push(result[0].childNodes[i]);
+      }
+      result = resultArray;      
+    }
+    
+    return result;
+  }
+  osn.RenderAll = osn.renderAll;
+  
+  osn.Html = function(node) {
+    var html = node.code ? "" + node.code : node.getAttribute("code") || "";
+    // TODO(levik): Sanitize the HTML here to avoid script injection issues.
+    // Perhaps use the gadgets sanitizer if available.
+    return html;
+  }
+  
+  function createClosure(object, method) {
+    return function() {
+      method.apply(object);
+    }
+  }
+  
+  function processOnAttach(node, code, data, context) {
+    var callbacks = context.getVariable(os.VAR_callbacks);
+    var func = new Function(code);
+    callbacks.push(createClosure(node, func));
+  }
+  os.registerAttribute("onAttach", processOnAttach);
+};
+
+os.defineBuiltinTags();

Added: incubator/shindig/trunk/features/opensocial-templates/os.js
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/features/opensocial-templates/os.js?rev=694034&view=auto
==============================================================================
--- incubator/shindig/trunk/features/opensocial-templates/os.js (added)
+++ incubator/shindig/trunk/features/opensocial-templates/os.js Wed Sep 10 15:21:55 2008
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+/**
+ * @fileoverview Implements os:renderAll tag and OpenSocial-specific
+ * identifier resolver.
+ */
+
+/**
+ * Identifier Resolver function for OpenSocial objects.
+ * Checks for:
+ * <ul>
+ *   <li>Simple property</li>
+ *   <li>JavaBean-style getter</li>
+ *   <li>OpenSocial Field</li>
+ *   <li>Data result set</li>
+ * </ul>
+ * @param {Object} object The object in the scope of which to get a named 
+ * property.
+ * @param {string} name The name of the property to get.
+ * @return {Object?} The property requested. 
+ */
+os.resolveOpenSocialIdentifier = function(object, name) {
+  // Simple property from object.
+  if (typeof(object[name]) != "undefined") {
+    return object[name];
+  }
+
+  // JavaBean-style getter method.
+  var functionName = os.getPropertyGetterName(name);
+  if (object[functionName]) {
+    return object[functionName]();
+  }
+
+  // Check OpenSocial field by dictionary mapping
+  if (object.getField) {
+    var fieldData = object.getField(name);
+    if (fieldData) {
+      return fieldData;
+    }
+  }
+
+  // Multi-purpose get() method
+  if (object.get) {
+    var responseItem = object.get(name);
+
+    // ResponseItem is a data set
+    if (responseItem && responseItem.getData) {
+      var data = responseItem.getData();
+      // Return array payload where appropriate
+      return data.array_ || data;
+    }
+    return responseItem;
+  }
+  
+  // Return undefined value, to avoid confusing with existing value of "null".
+  var und;
+  return und;
+};
+
+os.setIdentifierResolver(os.resolveOpenSocialIdentifier);
+
+/**
+ * Create methods for an object based upon a field map for OpenSocial.
+ * @param {Object} object Class object to have methods created for.
+ * @param {Object} fields A key-value map object to retrieve fields (keys) and
+ * method names (values) from.
+ * @private
+ */
+os.createOpenSocialGetMethods_ = function(object, fields) {
+  if (object && fields) {
+    for (var key in fields) {
+      var value = fields[key];
+      var getter = os.getPropertyGetterName(value);
+      object.prototype[getter] = function() {
+        this.getField(key);
+      }
+    }
+  }
+};
+
+/**
+ * Automatically register JavaBean-style methods for various OpenSocial objects.
+ * @private
+ */
+os.registerOpenSocialFields_ = function() {
+  var fields = os.resolveOpenSocialIdentifier.FIELDS;
+  if (opensocial) {
+    // TODO: Add more OpenSocial objects.
+    if (opensocial.Person) {
+      //os.createOpenSocialGetMethods_(opensocial.Person,  opensocial.Person.Field);
+    }
+  }
+};
+
+os.registerOpenSocialFields_();

Added: incubator/shindig/trunk/features/opensocial-templates/os_test.js
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/features/opensocial-templates/os_test.js?rev=694034&view=auto
==============================================================================
--- incubator/shindig/trunk/features/opensocial-templates/os_test.js (added)
+++ incubator/shindig/trunk/features/opensocial-templates/os_test.js Wed Sep 10 15:21:55 2008
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+/**
+ * Unit test for testing the behavior of the OpenSocial identifier resolver.
+ */
+function testResolveOpenSocialIdentifier() {
+  /**
+   * Sample class with a property, property getter, custom getField and a get()
+   * method.
+   */
+  var TestClass = function() {
+    this.foo = 'fooData';
+    this.bar_ = 'barData';
+    this.thumbnailUrl_ = 'thumbnailUrlData';
+    this.responseItem_ = {};
+    this.responseItem_.getData = function() {
+      return 'responseItemData';
+    };
+  };
+  TestClass.prototype.getBar = function() {
+    return this.bar_;
+  };
+  TestClass.prototype.getField = function(field) {
+    if (field == 'THUMBNAIL_URL') {
+      return this.thumbnailUrl_;
+    }
+    return null;
+  };
+  TestClass.prototype.get = function(field) {
+    if (field == 'responseItem') {
+      return this.responseItem_;
+    }
+    return null;
+  };
+  
+  var obj = new TestClass(); 
+  
+  assertEquals('fooData', os.resolveOpenSocialIdentifier(obj, 'foo'));
+  assertEquals('barData', os.resolveOpenSocialIdentifier(obj, 'bar'));
+  assertEquals('thumbnailUrlData',
+      os.resolveOpenSocialIdentifier(obj, 'THUMBNAIL_URL'));
+  assertEquals('responseItemData',
+      os.resolveOpenSocialIdentifier(obj, 'responseItem'));
+}

Added: incubator/shindig/trunk/features/opensocial-templates/ost_test.html
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/features/opensocial-templates/ost_test.html?rev=694034&view=auto
==============================================================================
--- incubator/shindig/trunk/features/opensocial-templates/ost_test.html (added)
+++ incubator/shindig/trunk/features/opensocial-templates/ost_test.html Wed Sep 10 15:21:55 2008
@@ -0,0 +1,127 @@
+<!--
+Copyright 2006 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<html>
+  <head>
+    <title>OpenSocial templates JsUnit tests</title>
+    <script src="jsunit/app/jsUnitCore.js"></script>
+    <script src="jsunit/app/jsUnitMockTimeout.js"></script>
+    <!-- the set of js files that make up opensocial-templates feature, as they appear in feature.xml -->
+    <script src="http://google-jstemplate.googlecode.com/svn/trunk/util.js"></script>
+    <script src="http://google-jstemplate.googlecode.com/svn/trunk/jsevalcontext.js"></script>
+    <script src="http://google-jstemplate.googlecode.com/svn/trunk/jstemplate.js"></script>
+    <script src="jstemplate_debug.js"></script>
+    <script src="base.js"></script>
+    <script src="namespaces.js"></script>
+    <script src="util.js"></script>
+    <script src="template.js"></script>
+    <script src="compiler.js"></script>
+    <script src="loader.js"></script>
+    <script src="container.js"></script>
+    <script src="os.js"></script>
+    <script src="data.js"></script>
+    <!-- the JsUnit tests -->
+    <script src="domutil.js"></script>
+    <script type="text/javascript" src="compiler_test.js"></script>
+    <script type="text/javascript" src="data_test.js"></script>
+    <script type="text/javascript" src="container_test.js"></script>
+    <script type="text/javascript" src="loader_test.js"></script>
+    <script type="text/javascript" src="os_test.js"></script>
+    <script type="text/javascript" src="util_test.js"></script>
+    <script type="text/javascript" src="template_test.js"></script>
+  </head>
+  <body>
+    <script type="text/os-template" tag="os:Test">tag template</script>
+    <script type="text/os-template">
+      <div id="test"><os:Test/></div>
+    </script>
+
+    <div style="display: none">
+      <div id="domSource">
+        <ul>
+          <li>one</li>
+          <li>two</li>
+        </ul>
+        <b>bold</b>
+      </div>
+      <div id="domTarget">
+      </div>
+    </div>
+
+    <xmp id="_T_Substitution_text" style="display: none">${title}:${value}</xmp>
+
+    <xmp id="_T_Substitution_text" style="display: none">${title}:${value}</xmp>
+    <xmp id="_T_Substitution_attribute" style="display: none">
+      <button id="${id}" style="color: ${color}" a1="value ${A1}">${text}</button>
+    </xmp>
+    <xmp id="my:user" style="display: none">
+      <a href="${url}">${name}</a> (${$my.foo})
+    </xmp>
+    <xmp id="my:record" style="display: none">
+      <b style="color: ${$my.color}">${title}</b>: <my:user context="user" foo="${$my.foo}"/>
+    </xmp>
+    <xmp id="_T_Substitution_nested" style="display: none">
+      <div repeat="users">
+        <my:record color="${color}" foo="${user.id}"/>
+      </div>
+    </xmp>
+
+    <xmp id="_T_Conditional_Number" style="display: none">
+      <span if="42==42">TRUE</span>
+      <span if="!(42==42)">FALSE</span>
+    </xmp>
+    <xmp id="_T_Conditional_String" style="display: none">
+      <span if="'101'=='101'">TRUE</span>
+      <span if="'101'!='101'">FALSE</span>
+    </xmp>
+    <xmp id="_T_Conditional_Mixed" style="display: none">
+      <span if="'101'>42">TRUE</span>
+      <span if="!('101'>42)">FALSE</span>
+    </xmp>
+
+    <xmp id="_T_Repeat" style="display: none">
+      <div repeat="entries">
+        ${data}
+      </div>
+    </xmp>
+
+    <xmp id="_T_Options" style="display: none">
+      <select id="options">
+        <option repeat="options" value="${value}">${value}</option>
+      </select>
+    </xmp>
+
+    <xmp id="custom:list" style="display: none">
+      <div repeat="$my.item"><os:renderAll content="title"/><os:renderAll content="body"/></div>
+    </xmp>
+
+    <xmp id="_T_List" style="display: none">
+      <custom:list>
+        <item>
+          <title>hello</title>
+          <body>world</body>
+        </item>
+      </custom:list>
+    </xmp>  
+
+    <xmp id="_T_Tag_input" style="display: none">
+      <!-- Uses a controller to grab the value ${def} as the default -->
+      <custom:input value="data"/>
+    </xmp>
+    <xmp id="_T_Tag_blink" style="display: none">
+      <custom:blink>blink text</custom:blink>
+    </xmp>
+  </body>
+</html>

Added: incubator/shindig/trunk/features/opensocial-templates/template.js
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/features/opensocial-templates/template.js?rev=694034&view=auto
==============================================================================
--- incubator/shindig/trunk/features/opensocial-templates/template.js (added)
+++ incubator/shindig/trunk/features/opensocial-templates/template.js Wed Sep 10 15:21:55 2008
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+/**
+ * @fileoverview Provides the Template class used to represent a single
+ * compiled template that can be rendered into any DOM node.
+ */
+
+/**
+ * A renderable compiled Template. A template can contain one or more
+ * compiled nodes pre-processed for JST operation. 
+ * @constructor
+ */
+os.Template = function(opt_id) {
+  this.templateRoot_ = document.createElement("span");
+  this.id = opt_id || ('template_' + os.Template.idCounter_++);
+};
+
+/**
+ * A global counter for template IDs.
+ * @type {number}
+ * @private
+ */
+os.Template.idCounter_ = 0;
+
+/**
+ * A Map of registered templates by keyed ID.
+ * @type {Object.<string|os.Template>}
+ * @private 
+ */
+os.registeredTemplates_ = {};
+
+/**
+ * Registers a compiled template by its ID.
+ * @param {os.Template} template List of template nodes.
+ */
+os.registerTemplate = function(template) {
+  os.registeredTemplates_[template.id] = template;
+};
+
+/**
+ * De-registers a compiled template..
+ * @param {os.Template} template List of template nodes.
+ */
+os.unRegisterTemplate = function(template) {
+  delete os.registeredTemplates_[template.id];
+};
+
+/**
+ * Gets a registered template by ID.
+ * @param {string} templateId The ID of a registered Template.
+ * @return {os.Template} A Template object.
+ */
+os.getTemplate = function(templateId) {
+  return os.registeredTemplates_[templateId];
+};
+
+/**
+ * Sets a single compiled node into this template.
+ * @param node {Element} A compiled node.
+ */
+os.Template.prototype.setCompiledNode_ = function(node) {
+  os.removeChildren(this.templateRoot_);
+  this.templateRoot_.appendChild(node);
+};
+
+/**
+ * Sets a list of compiled nodes into this template.
+ * @param nodes {Array.Element} An array of compiled nodes.
+ */
+os.Template.prototype.setCompiledNodes_ = function(nodes) {
+  os.removeChildren(this.templateRoot_);
+  for (var i = 0; i < nodes.length; i++) {
+    this.templateRoot_.appendChild(nodes[i]);
+  }
+};
+
+/**
+ * Renders the template and returns the result.
+ * Does not fire callbacks.
+ */
+os.Template.prototype.render = function(opt_data, opt_context) {
+  if (!opt_context) {
+    opt_context = os.createContext(opt_data);
+  }
+  return os.renderTemplateNode_(this.templateRoot_, opt_context);            
+};
+
+/**
+ * Renders the template and puts the result into the specified element, then
+ * fires callbacks.
+ */
+os.Template.prototype.renderInto = function(root, opt_data, opt_context) {
+  if (!opt_context) {
+    opt_context = os.createContext(opt_data);
+  }
+  var result = this.render(opt_data, opt_context);
+  os.removeChildren(root);
+  os.appendChildren(result, root);
+  os.fireCallbacks(opt_context);
+};

Added: incubator/shindig/trunk/features/opensocial-templates/template_test.js
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/features/opensocial-templates/template_test.js?rev=694034&view=auto
==============================================================================
--- incubator/shindig/trunk/features/opensocial-templates/template_test.js (added)
+++ incubator/shindig/trunk/features/opensocial-templates/template_test.js Wed Sep 10 15:21:55 2008
@@ -0,0 +1,740 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+/**
+ * Helper functions.
+ * @param {string} templateId The id of template definition node.
+ * @param {Object} opt_context The context data.
+ * @return {Element} The rendered HTML element.
+ * @private
+ */
+function compileAndRender_(templateId, opt_context) {
+  var template = os.compileTemplate(document.getElementById(templateId));
+
+  // Process the template and output the result
+  var outputNode = document.createElement("div");
+  template.renderInto(outputNode, opt_context);
+  return outputNode;
+}
+
+/**
+ * Takes a string representing a the markup of single DOM node, and returns
+ * the corresponding DOM node.
+ * 
+ * @param {string} markup Markup text of the DOM node
+ * @return {Node} The DOM node
+ */
+function markupToNode(markup) {
+  var node = document.createElement('div');
+  node.innerHTML = markup;
+  return node.firstChild;
+}
+
+/**
+ * Finds if an Attr object is real (specified and not inserted by JST).
+ * The JST attributes start with 'js', but IE also has a special '__jstcache'
+ * attribute.
+ * @param {Node} attr The Attr node object.
+ */
+function isRealAttribute(attr) {
+  return attr.specified &&
+         attr.name.indexOf('js') != 0 &&
+         attr.name != '__jstcache';
+};
+
+/**
+ * Normalize a node to the corresponding markup text, ignoring
+ *     JsTemplate artifacts:
+ * - Removes attributes starting with "js"
+ * - Removes the SPAN tag if it has a "customtag" attribute or has no attributes
+ *   that don't start with "js". This leaves the contents of the tag
+ * - Removes nodes with style="display: none;", which is what JsTemplate does
+ *   to nodes that aren't officially output.
+ * 
+ * @param {Node} node The DOM node
+ * @return {string} The normalized markup
+ */
+function nodeToNormalizedMarkup(node) {
+  if (node.nodeType == 3) {
+    return node.nodeValue;
+  } else if (node.nodeType == 1) {
+    var hasRealAttributes = false;
+
+    for (var i = 0; i < node.attributes.length; i++) {
+      if (isRealAttribute(node.attributes[i])) {
+        hasRealAttributes = true;
+      }
+    }
+    
+    if (node.getAttribute('customtag') != null) {
+      hasRealAttributes = false;
+    }
+    
+    if (node.nodeName == 'SPAN' && !hasRealAttributes) {
+      var text = '';
+      for (var i = 0; i < node.childNodes.length; i++) {
+        text += nodeToNormalizedMarkup(node.childNodes[i]);
+      }
+      return text;
+    }
+    if (node.style.display == 'none') {
+      return '';
+    }
+    
+    var text = '<' + node.nodeName;
+    for (var i = 0; i < node.attributes.length; i++) {
+      var att = node.attributes[i];
+      if (isRealAttribute(att)) {
+        text += ' ' + att.name + '="' + att.value + '"';
+      }
+    }
+    text += '>';
+    for (var i = 0; i < node.childNodes.length; i++) {
+      text += nodeToNormalizedMarkup(node.childNodes[i]);
+    }
+    text += '</' + node.nodeName + '>';
+    return text;
+  }
+  return '';
+}
+
+/**
+ * Normalizes a node or text string to normalized text of the DOM node
+ * 
+ * @param {Node|string} nodeOrText The DOM node or text of the DOM node
+ * @return {string} The normalized markup text
+ * 
+ */
+function normalizeNodeOrMarkup(nodeOrText) {
+  var node = typeof nodeOrText == 'string'
+    ? markupToNode(nodeOrText) : nodeOrText;
+  return nodeToNormalizedMarkup(node);
+}
+
+
+/*
+ * Checks if two DOM node are equal, ingoring template artefacts.
+ * 
+ * @param {Node|string} lhs First DOM node or string of markup contents
+ * @param {Node|string} rhs Second DOM node or string of markup contents
+ */
+function assertTemplateDomEquals(lhs, rhs) {
+  lhs = normalizeNodeOrMarkup(lhs);
+  rhs = normalizeNodeOrMarkup(rhs);
+  
+  assertEquals('DOM nodes not equal: \n_ ' + lhs + '\n_ ' + rhs, lhs, rhs);
+}
+
+
+/**
+ * Allow testing of templates passed in a strings. Allows for calling
+ * a template, passing in a map of named templates, and passing in the data
+ * context.
+ * 
+ * @param {string} templateText The text of the inline template to evaluate
+ * @param {string} output The expected output
+ * @param {Object?} context The data context
+ * @param {Array<String>?} namedTemplates Array of text of namedTemplates
+ */
+function assertTemplateOutput(templateText, output, context,
+    namedTemplates) {
+  
+  // Parse and register named templates
+  if (namedTemplates instanceof Array) {
+    for (var i = 0; i < namedTemplates.length; i++) {
+      var text = '<Templates xmlns:os="uri:unused">' + namedTemplates[i] +
+          '</Templates>';
+      var dom = os.parseXML_(text);
+      os.Loader.processTemplatesNode(dom);
+    }
+  }
+  
+  var template = os.compileTemplateString(templateText);
+  
+  // Process the template and output the result
+  var outputNode = document.createElement("div");
+  template.renderInto(outputNode, context);
+  assertTemplateDomEquals(output, outputNode.firstChild);
+}
+
+/**
+ * Tests Namespace.
+ */
+function testNamespace() {
+  // Create the "custom" namespace
+  var custom = os.createNamespace("custom", "http://google.com/#custom");
+  assertEquals(custom, os.getNamespace("custom"));
+
+  var custom_sameUrl =
+    os.createNamespace("custom", "http://google.com/#custom");
+  assertEquals(custom_sameUrl, os.getNamespace("custom"));
+
+  try {
+    var custom_newUrl =
+      os.createNamespace("custom", "http://google.com/#custom_new");
+    fail("no exception thrown with new URL for the same namespace");
+  }
+  catch (e) {
+    // We expect os to throw an exception for namespace conflict.
+    // But if e is a JsUnitException (thrown from fail), throw it again.
+    if (e.isJsUnitException) {
+      throw e;
+    }
+  }
+}
+
+/**
+ * Tests Substitution.
+ */
+function testSubstitution_text() {
+  var data = {
+    title: "count",
+    value: 0
+  };
+  var outputNode = compileAndRender_("_T_Substitution_text", data);
+
+  assertEquals(data.title + ":" + data.value,
+    domutil.getVisibleText(outputNode));
+}
+
+function testSubstitution_attribute() {
+  var data = {
+    id: "varInAttr",
+    color: "red",
+    A1: 111,
+    text: "click me"
+  };
+  var outputNode = compileAndRender_("_T_Substitution_attribute", data);
+  var contentNode = outputNode.firstChild;
+
+  assertEquals(data.id, contentNode.id);
+  assertEquals(data.color, contentNode.style.color);
+  assertEquals("value " + data.A1, contentNode.getAttribute("a1"));
+  assertEquals(data.text, contentNode.innerHTML);
+}
+
+function testSubstitution_nested() {
+  var data = {
+    title: "Users",
+    users: [
+      { title: "President", color: 'red',
+        user: { name: "Bob", id: "101", url: "http://www.bob.com" }},
+      { title: "Manager", color: 'green',
+        user: { name: "Rob", id: "102", url: "http://www.rob.com" }},
+      { title: "Peon", color: 'blue',
+        user: { name: "Jeb", id: "103", url: "http://www.jeb.com" }}
+    ]
+  };
+
+  os.createNamespace("my", "www.google.com/#my");
+  os.Container.registerTag('my:user');
+  os.Container.registerTag('my:record');
+
+  var outputNode = compileAndRender_("_T_Substitution_nested", data);
+
+  assertEquals(data.users.length, outputNode.childNodes.length);
+  for (var i = 0; i < data.users.length; i++) {
+    var user = data.users[i];
+    var divNode = outputNode.childNodes[i];
+    assertEquals("DIV", divNode.tagName);
+
+    // Find first Element child. FF creates an empty #text node, IE does not,
+    // so we need to look.
+    var spanNode = divNode.firstChild;
+    while (!spanNode.tagName) {
+      spanNode = spanNode.nextSibling;
+    }
+
+    assertEquals(user.color, spanNode.color);
+    assertEquals(user.user.id, spanNode.foo);
+
+    var recordNode = spanNode.childNodes[0];
+    assertEquals(user.color, recordNode.firstChild.style.color);
+    assertEquals(user.title, recordNode.firstChild.innerHTML);
+
+    var userNode = recordNode.lastChild;
+    assertEquals(user.user.id, userNode.foo);
+    var anchorNode = userNode.firstChild.childNodes[0];
+    assertEquals(user.user.name, anchorNode.innerHTML);
+    assertContains(user.user.url, anchorNode.href);
+
+    var fooNode = userNode.firstChild.childNodes[2];
+    assertEquals(user.user.id, fooNode.innerHTML);
+  }
+}
+
+/**
+ * Tests if attribute.
+ */
+function testConditional_Number() {
+  var outputNode = compileAndRender_("_T_Conditional_Number");
+
+  assertEquals("TRUE", domutil.getVisibleText(outputNode));
+}
+
+function testConditional_String() {
+  var outputNode = compileAndRender_("_T_Conditional_String");
+
+  assertEquals("TRUE", domutil.getVisibleText(outputNode));
+}
+
+function testConditional_Mixed() {
+  var outputNode = compileAndRender_("_T_Conditional_Mixed");
+
+  assertEquals("TRUE", domutil.getVisibleText(outputNode));
+}
+
+/**
+ * Tests repeat attribute.
+ */
+function testRepeat() {
+  var data = {
+    entries : [
+      { data: "This" },
+      { data: "is" },
+      { data: "an" },
+      { data: "array" },
+      { data: "of" },
+      { data: "data." }
+    ]
+  };
+  var outputNode = compileAndRender_("_T_Repeat", data);
+
+  assertEquals(data.entries.length, outputNode.childNodes.length);
+  for (var i = 0; i < data.entries.length; i++) {
+    var entry = data.entries[i];
+    assertEquals("DIV", outputNode.childNodes[i].tagName);
+    assertEquals(entry.data,
+      domutil.getVisibleTextTrim(outputNode.childNodes[i]));
+  }
+}
+
+/**
+ * Tests select elements.
+ */
+function testSelect() {
+  var data = {
+    options : [
+      { value: "one" },
+      { value: "two" },
+      { value: "three" }
+    ]
+  };
+  var outputNode = compileAndRender_("_T_Options", data);
+  var selectNode = outputNode.firstChild;
+  
+  assertEquals(data.options.length, selectNode.options.length);
+  for (var i = 0; i < data.options.length; i++) {
+    var entry = data.options[i];
+    var optionNode = selectNode.options[i];
+    assertEquals("OPTION", optionNode.tagName);
+    assertEquals(entry.value, optionNode.getAttribute('value'));
+  }
+}
+
+function testList() {
+  os.Container.registerTag('custom:list');
+  var output = compileAndRender_('_T_List');
+  assertEquals('helloworld', domutil.getVisibleText(output));
+}
+
+/**
+ * Tests JS custom tags.
+ */
+function testTag_input() {
+  var custom = os.createNamespace("custom", "http://google.com/#custom");
+  /**
+   * Custom controller that uses the value of any input fields as a key to
+   * replace itself with in the context data.
+   */
+  custom.input = function(node, context) { // return HTML;
+    var inputNode = document.createElement('input');
+
+    // Use the "value" attribute from the tag to index the context data
+    inputNode.value = context[node.getAttribute('value')];
+    return inputNode;
+  };
+
+  var data = {
+    data: "Some default data"
+  };
+  var outputNode = compileAndRender_("_T_Tag_input", data);
+  // contentNode is wrapped by <div><span>
+  var contentNode = outputNode.firstChild.firstChild;
+  assertEquals(contentNode.value, data.data);
+}
+
+function testTag_blink() {
+  var custom = os.createNamespace("custom", "http://google.com/#custom");
+  /**
+   * A controller that reproduces blink behavior.
+   */
+  custom.blink = function(node, context) { // returns HTML
+    var root = document.createElement("span");
+    root.appendChild(node.firstChild);
+    function blink() {
+      var isVisible = root.style["visibility"] == "visible";
+      root.style["visibility"] = isVisible ? "hidden" : "visible";
+      setTimeout(blink, 500);
+    }
+    root.onAttach = function() {
+      blink();
+    }
+    return root;
+  }
+
+  Clock.reset();
+  var outputNode = compileAndRender_("_T_Tag_blink");
+  // contentNode is wrapped by <div><span>
+  var contentNode = outputNode.firstChild.firstChild;
+  assertEquals(contentNode.style.visibility, "visible");
+  Clock.tick(500);
+  assertEquals(contentNode.style.visibility, "hidden");
+  Clock.tick(500);
+  assertEquals(contentNode.style.visibility, "visible");
+}
+
+function testHelloWorld() {
+  assertTemplateOutput(
+    '<div>Hello world!</div>',
+    '<div>Hello world!</div>');
+}
+
+function testSimpleExpression() {
+  assertTemplateOutput(
+    '<div>${HelloWorld}</div>',
+    '<div>Hello world!</div>',
+    {HelloWorld: 'Hello world!'});
+}
+
+function testNamedTemplate() {
+  assertTemplateOutput(
+    '<div><os:HelloWorld/></div>',
+    '<div>Hello world!</div>',
+    null,
+    ['<Template tag="os:HelloWorld">Hello world!</Template>']);
+}
+
+function testParameter() {
+  var tryTemplateContent = function(content) {
+    assertTemplateOutput(
+      '<div><os:HelloWorldWithParam text="Hello world!"/></div>',
+      '<div>Hello world!</div>',
+      null,
+      ['<Template tag="os:HelloWorldWithParam">' + content + '</Template>']);
+  }
+  
+  tryTemplateContent('${$my.text}');
+  tryTemplateContent('${My.text}');
+  tryTemplateContent('${my.text}');
+
+  // Not working yet:
+  /*
+  tryTemplateContent('${text}');
+  */
+}
+
+function testContent() {
+  var tryTemplateContent = function(content) {
+    assertTemplateOutput(
+      '<div><os:HelloWorldWithContent>Hello world!' +
+      '</os:HelloWorldWithContent></div>',
+      '<div>Hello world!</div>',
+      null,
+      ['<Template tag="os:HelloWorldWithContent">' + content + '</Template>']);
+  }
+    
+  tryTemplateContent('<os:renderAll/>');
+  tryTemplateContent('<os:RenderAll/>');
+}
+
+function testNamedContent() {
+  var tryTemplateContent = function(content) {
+    assertTemplateOutput(
+      '<div>' +
+      '<os:HelloWorldWithNamedContent>' +
+        '<os:DontShowThis>Don\'t show this</os:DontShowThis>' +
+        '<os:Content>Hello <b>world!</b></os:Content>' +
+        '<Content>Hello <b>world!</b></Content>' +
+      '</os:HelloWorldWithNamedContent>' +
+      '</div>',
+      
+      '<div>Hello <b>world!</b></div>',
+      null,
+      ['<Template tag="os:HelloWorldWithNamedContent">' + content + '</Template>']);
+  }
+  tryTemplateContent('<os:renderAll content="os:Content"/>');
+  tryTemplateContent('<os:renderAll content="Content"/>');
+  
+  // Not working yet:
+  /*
+  tryTemplateContent('<os:renderAll content="${my.os:Content}"/>');
+  tryTemplateContent('<os:renderAll content="my.os:Content"/>');
+  tryTemplateContent('<os:renderAll content="${My.os:Content}"/>');
+  tryTemplateContent('<os:renderAll content="${my.Content}"/>');
+  tryTemplateContent('<os:renderAll content="my.Content"/>');
+  tryTemplateContent('<os:renderAll content="${My.Content}"/>');
+  tryTemplateContent('<os:renderAll content="${Content}"/>');
+  */
+}
+
+function testRepeatedContent() {
+  var tryTemplateContent = function(content) {
+    assertTemplateOutput(
+      '<os:HelloWorldRepeatedContent>' +
+        '<Word>Hello</Word>' +
+        '<Word>world!</Word>' +
+        '<os:Word>Hello</os:Word>' +
+        '<os:Word>world!</os:Word>' +
+      '</os:HelloWorldRepeatedContent>',
+      
+      '<div>Helloworld!</div>',
+      null,
+      ['<Template tag="os:HelloWorldRepeatedContent">' + content + '</Template>']);
+  };
+
+  tryTemplateContent('<div><span repeat="$my.Word"><os:renderAll/></span></div>');
+  tryTemplateContent('<div><span repeat="${$my.Word}"><os:renderAll/></span></div>');
+  tryTemplateContent('<div><span repeat="${$my.os:Word}"><os:renderAll/></span></div>');
+  tryTemplateContent('<div><span repeat="$my.os:Word"><os:renderAll/></span></div>');
+  tryTemplateContent('<div><span repeat="${my.os:Word}"><os:renderAll/></span></div>');
+  tryTemplateContent('<div><span repeat="$my.os:Word"><os:renderAll/></span></div>');
+  tryTemplateContent('<div><span repeat="${My.os:Word}"><os:renderAll/></span></div>');
+
+  // Not working yet because $my must be explicit:
+  /*
+    tryTemplateContent('<div><span repeat="${Word}"><os:renderAll/></span></div>');
+    tryTemplateContent('<div><span repeat="${os:Word}"><os:renderAll/></span></div>');
+    tryTemplateContent('<div><span repeat="Word"><os:renderAll/></span></div>');
+    tryTemplateContent('<div><span repeat="os:Word"><os:renderAll/></span></div>');
+  */
+};
+
+/**
+ * Bug when calling a repeat twice - 2nd time fails
+ *
+ * This is because <os:renderAll> moves the child tags out to destination, so
+ * the second loop is empty.
+ */
+function testRepeatedContentTwice() {
+  /*
+  assertTemplateOutput(
+    '<os:HelloWorldRepeatedContent>' +
+      '<os:Word>Hello</os:Word>' +
+      '<os:Word>world!</os:Word>' +
+    '</os:HelloWorldRepeatedContent>',
+    
+    '<div><div>Helloworld!</div><div>Helloworld!</div></div>',
+    null,
+    ['<Template tag="os:HelloWorldRepeatedContent">
+      '<div>' +
+      '<div><span repeat="$my.os:Word"><os:renderAll/></span></div>' +
+      '<div><span repeat="$my.os:Word"><os:renderAll/></span></div>' +
+      '</Template>']
+    );
+  */
+};
+
+/**
+ * Currently, expression inside "content" attribute is equiv of no attribute.
+ * Probably should just include no data.
+ * 
+ * I.e.
+ * <os:renderAll content="${os:NoMatch}"/> currently has same output as
+ * <os:renderAll/>
+ */
+function testRenderAllBadExprInContent() {
+  /*
+  assertTemplateOutput(
+    '<div>' +
+    '<os:HelloWorldBadExpr>' +
+      '<os:Content>Hello world!</os:Content>' +
+    '</os:HelloWorldBadExpr>' +
+    '</div>',
+    
+    '<div></div>',
+    null,
+    ['<Template tag="os:HelloWorldBadExpr">' + 
+     '<os:renderAll content="${os:NoMatch}"/>' +
+     '</Template>']);
+  */
+}
+
+
+function testBooleanTrue() {
+  assertTemplateOutput(
+    '<span if="${BooleanTrue}">Hello world!</span>',
+    '<span>Hello world!</span>',
+    {BooleanTrue: true});
+    
+  assertTemplateOutput(
+    '<span if="BooleanTrue">Hello world!</span>',
+    '<span>Hello world!</span>',
+    {BooleanTrue: true});
+    
+  assertTemplateOutput(
+    '<span if="!BooleanTrue">Hello world!</span>',
+    '<span></span>',
+    {BooleanTrue: true});
+    
+  assertTemplateOutput(
+    '<span if="${!BooleanTrue}">Hello world!</span>',
+    '<span></span>',
+    {BooleanTrue: true});
+}
+
+
+function testBooleanFalse() {
+  assertTemplateOutput(
+    '<span if="BooleanFalse">Hello world!</span>',
+    '<span></span>',
+    {BooleanFalse: false});
+    
+  assertTemplateOutput(
+    '<span if="!BooleanFalse">Hello world!</span>',
+    '<span>Hello world!</span>',
+    {BooleanFalse: false});
+    
+  assertTemplateOutput(
+    '<span if="${!BooleanFalse}">Hello world!</span>',
+    '<span>Hello world!</span>',
+    {BooleanFalse: false});
+
+  assertTemplateOutput(
+    '<span if="${BooleanFalse}">Hello world!</span>',
+    '<span></span>',
+    {BooleanFalse: false});
+}
+
+
+function testRepeatedNode() {
+  var tryTemplateContent = function(content) {
+    assertTemplateOutput(
+      content,
+      '<div>Helloworld!</div>',
+      {
+        Words: ['Hello', 'world!'],
+        WordObjects: [{value: 'Hello'}, {value: 'world!'}]
+      });
+  }
+  
+  tryTemplateContent('<div><span repeat="WordObjects">${$cur.value}</span></div>');
+  tryTemplateContent('<div><span repeat="WordObjects">${value}</span></div>');
+  tryTemplateContent('<div><span repeat="WordObjects">${cur.value}</span></div>');
+  tryTemplateContent('<div><span repeat="Words">${cur}</span></div>');
+  tryTemplateContent('<div><span repeat="Words">${$cur}</span></div>');
+  tryTemplateContent('<div><span repeat="Words">${Cur}</span></div>');
+
+  // Do we want to continue to support this?
+  tryTemplateContent('<div><span repeat="Words">${$this}</span></div>');
+};
+
+function testDynamicRepeatedContent() {
+ 
+  assertTemplateOutput(
+    '<os:DynamicRepeat>' +
+      '<Word repeat="WordObjects">${$cur.value}</Word>' +
+    '</os:DynamicRepeat>',
+    '<div>Helloworld!</div>',
+    {WordObjects: [{value: 'Hello'}, {value: 'world!'}]},
+
+    ['<Template tag="os:DynamicRepeat">' + 
+     '<div><span repeat="$my.Word"><os:renderAll/></span></div>' +
+     '</Template>']);
+
+};
+
+function testReplaceTopLevelVars() {
+  function test(src, dest) {
+    assertEquals(dest, os.replaceTopLevelVars_(src));
+  }
+  
+  // Basic substitution for each replacement
+  test('my.man', '$my.man');
+  test('my', '$my');
+  test('My.man', '$my.man');
+  test('My', '$my');
+  test('cur.man', '$this.man');
+  test('cur', '$this');
+  test('Cur.man', '$this.man');
+  test('Cur', '$this');
+  
+  // Basic no sustitution
+  test('$my.man', '$my.man');
+  test('$my', '$my');
+  test('ns.My', 'ns.My');
+  test('Cur/2', '$this/2');
+  test('Cur*2', '$this*2');
+  test('Cur[My.name]', '$this[$my.name]');
+  test('Cur||\'Nothing\'', '$this||\'Nothing\'');
+  
+  // Single operator, both fist and last expression
+  test('My.man+your.man', '$my.man+your.man');
+  test('your.man>My.man', 'your.man>$my.man');
+  
+  // Tests a specific operator
+  function testOperator(operator) {
+    test('My.man' + operator + 'your.man',
+        '$my.man' + operator + 'your.man');
+        
+    test('your.man' + operator + 'My.man',
+        'your.man' + operator + '$my.man');
+    
+    test('My' + operator + 'My',
+        '$my' + operator + '$my');
+  }
+  
+  // All operators
+  testOperator('+');
+  testOperator(' + ');
+  testOperator('-');
+  testOperator('<');
+  testOperator(' lt ');
+  testOperator('>');
+  testOperator(' gt ');
+  testOperator('=');
+  testOperator('!=');
+  testOperator('==');
+  testOperator('&&');
+  testOperator(' and ');
+  testOperator('||');
+  testOperator(' or ');
+  testOperator(' and !');
+  testOperator('/');
+  testOperator('*');
+  testOperator('|');
+  testOperator('(');
+  testOperator('[');
+};
+
+function testHtmlTag() {
+  var template = os.compileTemplateString('<os:Html code="${foo}"/>');
+  var output = template.render({foo: 'Hello <b>world</b>!'});
+  var boldNodes = output.getElementsByTagName("b");
+  assertEquals(1, boldNodes.length); 
+};
+
+function testOnAttachAttribute() {
+  /* fails on IE.
+  var template = os.compileTemplateString(
+      '<div onAttach="this.title=\'bar\'"/>');
+  var output = document.createElement('div');
+  template.renderInto(output);
+  assertEquals('bar', output.firstChild.title);
+  */
+};

Added: incubator/shindig/trunk/features/opensocial-templates/util.js
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/features/opensocial-templates/util.js?rev=694034&view=auto
==============================================================================
--- incubator/shindig/trunk/features/opensocial-templates/util.js (added)
+++ incubator/shindig/trunk/features/opensocial-templates/util.js Wed Sep 10 15:21:55 2008
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+/**
+ * @fileoverview Provides various utility functions used throughout the library.
+ */
+
+/**
+ * Checks whether or not a given character is alpha-numeric.
+ * @param {string} ch Character to check.
+ * @return {boolean} This character is alpha-numeric.
+ */
+os.isAlphaNum = function(ch) {
+  return ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
+      (ch >= '0' && ch <= '9') || ch == '_');
+};
+
+/**
+ * Clears the children of a given DOM node.
+ * @param {Node} node DOM node to clear.
+ */
+os.removeChildren = function(node) {
+  while (node.firstChild) {
+    node.removeChild(node.firstChild);
+  }
+};
+
+/**
+ * Clears the children of a given DOM node.
+ * @param {Node} sourceNode DOM node with children to append.
+ * @param {Node} targetNode DOM node to append sourceNode's children to.
+ */
+os.appendChildren = function(sourceNode, targetNode) {
+  while (sourceNode.firstChild) {
+    targetNode.appendChild(sourceNode.firstChild);
+  }
+};
+
+/**
+ * Replaces a specified DOM node with other node(s).
+ * @param {Element} node The node to be replaced
+ * @param {Element|Array.<Element>} replacement Can be a node or an array of 
+ *     nodes. Elements can be TextNodes.
+ */
+os.replaceNode = function(node, replacement) {
+  var parent = node.parentNode;
+  if (!parent) {
+    throw 'Error in replaceNode() - Node has no parent: ' + node;
+  }
+  if (replacement.nodeType == DOM_ELEMENT_NODE ||
+      replacement.nodeType == DOM_TEXT_NODE) {
+    parent.replaceChild(replacement, node);
+  } else if (isArray(replacement)) {
+    for (var i = 0; i < replacement.length; i++) {
+      parent.insertBefore(replacement[i], node);
+    }
+    parent.removeChild(node);
+  }
+};
+
+/**
+ * Given a property name (e.g. 'foo') will create a JavaBean-style getter 
+ * (e.g. 'getFoo').
+ * @param {string} propertyName Name of the property.
+ * @return {string} The name of the getter function.
+ */
+os.getPropertyGetterName = function(propertyName) {
+  var getter = 'get' + propertyName.charAt(0).toUpperCase() +
+      propertyName.substring(1);
+  return getter;
+};
+
+
+/**
+ * Given a constant-style string (e.g., 'FOO_BAR'), will return a camel-cased
+ * string (e.g., fooBar).
+ * @param {string} str String to convert to camelCase.
+ * @return {string} The camel-cased string.
+ */
+os.convertToCamelCase = function(str) {
+  var words = str.toLowerCase().split('_');
+  var out = [];
+  out.push(words[0].toLowerCase());
+  for (var i = 1; i < words.length; ++i) {
+    var piece = words[i].charAt(0).toUpperCase() + words[i].substring(1);
+    out.push(piece);
+  }
+  return out.join('');
+};

Added: incubator/shindig/trunk/features/opensocial-templates/util_test.js
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/features/opensocial-templates/util_test.js?rev=694034&view=auto
==============================================================================
--- incubator/shindig/trunk/features/opensocial-templates/util_test.js (added)
+++ incubator/shindig/trunk/features/opensocial-templates/util_test.js Wed Sep 10 15:21:55 2008
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+/**
+ * Unit test for various DOM utils.
+ */
+function testDomUtils() {
+  var sourceNode = document.getElementById('domSource');
+  var targetNode = document.getElementById('domTarget');
+  var html = sourceNode.innerHTML;
+  targetNode.innerHTML = '';
+
+  // test appendChildren
+  os.appendChildren(sourceNode, targetNode);
+  assertEquals(html, targetNode.innerHTML);
+
+  // test removeChildren
+  os.removeChildren(targetNode);
+  assertEquals(0, targetNode.childNodes.length);
+
+  // test replaceNode
+  var child = document.createElement('p');
+  sourceNode.appendChild(child);
+  os.replaceNode(child, document.createElement('div'));
+  assertEquals('DIV', sourceNode.firstChild.tagName);
+}
+
+/**
+ * Unit test for createPropertyGetter
+ */
+function testGetPropertyGetterName() {
+  assertEquals('getFoo', os.getPropertyGetterName('foo'));
+  assertEquals('getFooBar', os.getPropertyGetterName('fooBar'));
+}
+
+/**
+ * Unit test for convertConstantToCamelCase.
+ */
+function testConvertToCamelCase() {
+  assertEquals('foo', os.convertToCamelCase('FOO'));
+  assertEquals('fooBar', os.convertToCamelCase('FOO_BAR'));
+  assertEquals('fooBarBaz', os.convertToCamelCase('FOO_BAR__BAZ'));
+}