Bug 1266826 - make Templater.jsm eslint-clean; r?jryans draft
authorTom Tromey <tom@tromey.com>
Wed, 11 May 2016 13:51:17 -0600
changeset 365998 abf141c69b747183f0f8b56f93bd2bf6f77ceb70
parent 365987 45daaf6edeae80ec8c67da50fa1d31f4a1b1a454
child 365999 2c92bc71a8415cac34bdbdc30ec9b5467cd56cb2
push id17880
push userbmo:ttromey@mozilla.com
push dateWed, 11 May 2016 20:41:52 +0000
reviewersjryans
bugs1266826
milestone49.0a1
Bug 1266826 - make Templater.jsm eslint-clean; r?jryans MozReview-Commit-ID: KaboysVD8qr
devtools/shared/gcli/Templater.jsm
--- a/devtools/shared/gcli/Templater.jsm
+++ b/devtools/shared/gcli/Templater.jsm
@@ -9,23 +9,25 @@
  *
  * 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.
  */
 
+"use strict";
+
+/* globals document */
+
 this.EXPORTED_SYMBOLS = [ "template" ];
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "console",
                                   "resource://gre/modules/Console.jsm");
 
-'use strict';
-
 /**
  * For full documentation, see:
  * https://github.com/mozilla/domtemplate/blob/master/README.md
  */
 
 /**
  * Begin a new templating process.
  * @param node A DOM element or string referring to an element's id
@@ -38,39 +40,38 @@ XPCOMUtils.defineLazyModuleGetter(this, 
  *   engine maintains a stack of tasks to help debug where it is. This allows
  *   this stack to be prefixed with a template name
  * - blankNullUndefined: By default DOMTemplate exports null and undefined
  *   values using the strings 'null' and 'undefined', which can be helpful for
  *   debugging, but can introduce unnecessary extra logic in a template to
  *   convert null/undefined to ''. By setting blankNullUndefined:true, this
  *   conversion is handled by DOMTemplate
  */
-var template = function(node, data, options) {
-  var state = {
+var template = function (node, data, options) {
+  let state = {
     options: options || {},
     // We keep a track of the nodes that we've passed through so we can keep
     // data.__element pointing to the correct node
     nodes: []
   };
 
   state.stack = state.options.stack;
 
   if (!Array.isArray(state.stack)) {
-    if (typeof state.stack === 'string') {
+    if (typeof state.stack === "string") {
       state.stack = [ options.stack ];
-    }
-    else {
+    } else {
       state.stack = [];
     }
   }
 
   processNode(state, node, data);
 };
 
-if (typeof exports !== 'undefined') {
+if (typeof exports !== "undefined") {
   exports.template = template;
 }
 this.template = template;
 
 /**
  * Helper for the places where we need to act asynchronously and keep track of
  * where we are right now
  */
@@ -93,221 +94,209 @@ var TEMPLATE_REGION = /\$\{([^}]*)\}/g;
 
 /**
  * Recursive function to walk the tree processing the attributes as it goes.
  * @param node the node to process. If you pass a string in instead of a DOM
  * element, it is assumed to be an id for use with document.getElementById()
  * @param data the data to use for node processing.
  */
 function processNode(state, node, data) {
-  if (typeof node === 'string') {
+  if (typeof node === "string") {
     node = document.getElementById(node);
   }
   if (data == null) {
     data = {};
   }
-  state.stack.push(node.nodeName + (node.id ? '#' + node.id : ''));
-  var pushedNode = false;
+  state.stack.push(node.nodeName + (node.id ? "#" + node.id : ""));
+  let pushedNode = false;
   try {
     // Process attributes
     if (node.attributes && node.attributes.length) {
       // We need to handle 'foreach' and 'if' first because they might stop
       // some types of processing from happening, and foreach must come first
       // because it defines new data on which 'if' might depend.
-      if (node.hasAttribute('foreach')) {
+      if (node.hasAttribute("foreach")) {
         processForEach(state, node, data);
         return;
       }
-      if (node.hasAttribute('if')) {
+      if (node.hasAttribute("if")) {
         if (!processIf(state, node, data)) {
           return;
         }
       }
       // Only make the node available once we know it's not going away
       state.nodes.push(data.__element);
       data.__element = node;
       pushedNode = true;
       // It's good to clean up the attributes when we've processed them,
       // but if we do it straight away, we mess up the array index
-      var attrs = Array.prototype.slice.call(node.attributes);
-      for (var i = 0; i < attrs.length; i++) {
-        var value = attrs[i].value;
-        var name = attrs[i].name;
+      let attrs = Array.prototype.slice.call(node.attributes);
+      for (let i = 0; i < attrs.length; i++) {
+        let value = attrs[i].value;
+        let name = attrs[i].name;
 
         state.stack.push(name);
         try {
-          if (name === 'save') {
+          if (name === "save") {
             // Save attributes are a setter using the node
             value = stripBraces(state, value);
             property(state, value, data, node);
-            node.removeAttribute('save');
-          }
-          else if (name.substring(0, 2) === 'on') {
+            node.removeAttribute("save");
+          } else if (name.substring(0, 2) === "on") {
             // If this attribute value contains only an expression
-            if (value.substring(0, 2) === '${' && value.slice(-1) === '}' &&
-                    value.indexOf('${', 2) === -1) {
+            if (value.substring(0, 2) === "${" && value.slice(-1) === "}" &&
+                    value.indexOf("${", 2) === -1) {
               value = stripBraces(state, value);
-              var func = property(state, value, data);
-              if (typeof func === 'function') {
+              let func = property(state, value, data);
+              if (typeof func === "function") {
                 node.removeAttribute(name);
-                var capture = node.hasAttribute('capture' + name.substring(2));
+                let capture = node.hasAttribute("capture" + name.substring(2));
                 node.addEventListener(name.substring(2), func, capture);
                 if (capture) {
-                  node.removeAttribute('capture' + name.substring(2));
+                  node.removeAttribute("capture" + name.substring(2));
                 }
-              }
-              else {
+              } else {
                 // Attribute value is not a function - use as a DOM-L0 string
                 node.setAttribute(name, func);
               }
-            }
-            else {
+            } else {
               // Attribute value is not a single expression use as DOM-L0
               node.setAttribute(name, processString(state, value, data));
             }
-          }
-          else {
+          } else {
             node.removeAttribute(name);
             // Remove '_' prefix of attribute names so the DOM won't try
             // to use them before we've processed the template
-            if (name.charAt(0) === '_') {
+            if (name.charAt(0) === "_") {
               name = name.substring(1);
             }
 
             // Async attributes can only work if the whole attribute is async
-            var replacement;
-            if (value.indexOf('${') === 0 &&
-                value.charAt(value.length - 1) === '}') {
+            let replacement;
+            if (value.indexOf("${") === 0 &&
+                value.charAt(value.length - 1) === "}") {
               replacement = envEval(state, value.slice(2, -1), data, value);
-              if (replacement && typeof replacement.then === 'function') {
-                node.setAttribute(name, '');
+              if (replacement && typeof replacement.then === "function") {
+                node.setAttribute(name, "");
                 /* jshint loopfunc:true */
-                replacement.then(function(newValue) {
+                replacement.then(function (newValue) {
                   node.setAttribute(name, newValue);
                 }).then(null, console.error);
-              }
-              else {
+              } else {
                 if (state.options.blankNullUndefined && replacement == null) {
-                  replacement = '';
+                  replacement = "";
                 }
                 node.setAttribute(name, replacement);
               }
-            }
-            else {
+            } else {
               node.setAttribute(name, processString(state, value, data));
             }
           }
-        }
-        finally {
+        } finally {
           state.stack.pop();
         }
       }
     }
 
     // Loop through our children calling processNode. First clone them, so the
     // set of nodes that we visit will be unaffected by additions or removals.
-    var childNodes = Array.prototype.slice.call(node.childNodes);
-    for (var j = 0; j < childNodes.length; j++) {
+    let childNodes = Array.prototype.slice.call(node.childNodes);
+    for (let j = 0; j < childNodes.length; j++) {
       processNode(state, childNodes[j], data);
     }
 
-    if (node.nodeType === 3 /*Node.TEXT_NODE*/) {
+    /* 3 === Node.TEXT_NODE*/
+    if (node.nodeType === 3) {
       processTextNode(state, node, data);
     }
-  }
-  finally {
+  } finally {
     if (pushedNode) {
       data.__element = state.nodes.pop();
     }
     state.stack.pop();
   }
 }
 
 /**
  * Handle attribute values where the output can only be a string
  */
 function processString(state, value, data) {
-  return value.replace(TEMPLATE_REGION, function(path) {
-    var insert = envEval(state, path.slice(2, -1), data, value);
-    return state.options.blankNullUndefined && insert == null ? '' : insert;
+  return value.replace(TEMPLATE_REGION, function (path) {
+    let insert = envEval(state, path.slice(2, -1), data, value);
+    return state.options.blankNullUndefined && insert == null ? "" : insert;
   });
 }
 
 /**
  * Handle <x if="${...}">
  * @param node An element with an 'if' attribute
  * @param data The data to use with envEval()
  * @returns true if processing should continue, false otherwise
  */
 function processIf(state, node, data) {
-  state.stack.push('if');
+  state.stack.push("if");
   try {
-    var originalValue = node.getAttribute('if');
-    var value = stripBraces(state, originalValue);
-    var recurse = true;
+    let originalValue = node.getAttribute("if");
+    let value = stripBraces(state, originalValue);
+    let recurse = true;
     try {
-      var reply = envEval(state, value, data, originalValue);
+      let reply = envEval(state, value, data, originalValue);
       recurse = !!reply;
-    }
-    catch (ex) {
-      handleError(state, 'Error with \'' + value + '\'', ex);
+    } catch (ex) {
+      handleError(state, "Error with '" + value + "'", ex);
       recurse = false;
     }
     if (!recurse) {
       node.parentNode.removeChild(node);
     }
-    node.removeAttribute('if');
+    node.removeAttribute("if");
     return recurse;
-  }
-  finally {
+  } finally {
     state.stack.pop();
   }
 }
 
 /**
  * Handle <x foreach="param in ${array}"> and the special case of
  * <loop foreach="param in ${array}">.
  * This function is responsible for extracting what it has to do from the
  * attributes, and getting the data to work on (including resolving promises
  * in getting the array). It delegates to processForEachLoop to actually
  * unroll the data.
  * @param node An element with a 'foreach' attribute
  * @param data The data to use with envEval()
  */
 function processForEach(state, node, data) {
-  state.stack.push('foreach');
+  state.stack.push("foreach");
   try {
-    var originalValue = node.getAttribute('foreach');
-    var value = originalValue;
+    let originalValue = node.getAttribute("foreach");
+    let value = originalValue;
 
-    var paramName = 'param';
-    if (value.charAt(0) === '$') {
+    let paramName = "param";
+    if (value.charAt(0) === "$") {
       // No custom loop variable name. Use the default: 'param'
       value = stripBraces(state, value);
-    }
-    else {
+    } else {
       // Extract the loop variable name from 'NAME in ${ARRAY}'
-      var nameArr = value.split(' in ');
+      let nameArr = value.split(" in ");
       paramName = nameArr[0].trim();
       value = stripBraces(state, nameArr[1].trim());
     }
-    node.removeAttribute('foreach');
+    node.removeAttribute("foreach");
     try {
-      var evaled = envEval(state, value, data, originalValue);
-      var cState = cloneState(state);
-      handleAsync(evaled, node, function(reply, siblingNode) {
+      let evaled = envEval(state, value, data, originalValue);
+      let cState = cloneState(state);
+      handleAsync(evaled, node, function (reply, siblingNode) {
         processForEachLoop(cState, reply, node, siblingNode, data, paramName);
       });
       node.parentNode.removeChild(node);
+    } catch (ex) {
+      handleError(state, "Error with " + value + "'", ex);
     }
-    catch (ex) {
-      handleError(state, 'Error with \'' + value + '\'', ex);
-    }
-  }
-  finally {
+  } finally {
     state.stack.pop();
   }
 }
 
 /**
  * Called by processForEach to handle looping over the data in a foreach loop.
  * This works with both arrays and objects.
  * Calls processForEachMember() for each member of 'set'
@@ -315,23 +304,22 @@ function processForEach(state, node, dat
  * @param templNode The node to copy for each set member
  * @param sibling The sibling node to which we add things
  * @param data the data to use for node processing
  * @param paramName foreach loops have a name for the parameter currently being
  * processed. The default is 'param'. e.g. <loop foreach="param in ${x}">...
  */
 function processForEachLoop(state, set, templNode, sibling, data, paramName) {
   if (Array.isArray(set)) {
-    set.forEach(function(member, i) {
+    set.forEach(function (member, i) {
       processForEachMember(state, member, templNode, sibling,
-                           data, paramName, '' + i);
+                           data, paramName, "" + i);
     });
-  }
-  else {
-    for (var member in set) {
+  } else {
+    for (let member in set) {
       if (set.hasOwnProperty(member)) {
         processForEachMember(state, member, templNode, sibling,
                              data, paramName, member);
       }
     }
   }
 }
 
@@ -342,102 +330,99 @@ function processForEachLoop(state, set, 
  * clone the template node, and pass the processing on to processNode().
  * @param member The data item to use in templating
  * @param templNode The node to copy for each set member
  * @param siblingNode The parent node to which we add things
  * @param data the data to use for node processing
  * @param paramName The name given to 'member' by the foreach attribute
  * @param frame A name to push on the stack for debugging
  */
-function processForEachMember(state, member, templNode, siblingNode, data, paramName, frame) {
+function processForEachMember(state, member, templNode, siblingNode, data,
+                              paramName, frame) {
   state.stack.push(frame);
   try {
-    var cState = cloneState(state);
-    handleAsync(member, siblingNode, function(reply, node) {
+    let cState = cloneState(state);
+    handleAsync(member, siblingNode, function (reply, node) {
       // Clone data because we can't be sure that we can safely mutate it
-      var newData = Object.create(null);
-      Object.keys(data).forEach(function(key) {
+      let newData = Object.create(null);
+      Object.keys(data).forEach(function (key) {
         newData[key] = data[key];
       });
       newData[paramName] = reply;
       if (node.parentNode != null) {
-        var clone;
-        if (templNode.nodeName.toLowerCase() === 'loop') {
-          for (var i = 0; i < templNode.childNodes.length; i++) {
+        let clone;
+        if (templNode.nodeName.toLowerCase() === "loop") {
+          for (let i = 0; i < templNode.childNodes.length; i++) {
             clone = templNode.childNodes[i].cloneNode(true);
             node.parentNode.insertBefore(clone, node);
             processNode(cState, clone, newData);
           }
-        }
-        else {
+        } else {
           clone = templNode.cloneNode(true);
-          clone.removeAttribute('foreach');
+          clone.removeAttribute("foreach");
           node.parentNode.insertBefore(clone, node);
           processNode(cState, clone, newData);
         }
       }
     });
-  }
-  finally {
+  } finally {
     state.stack.pop();
   }
 }
 
 /**
  * Take a text node and replace it with another text node with the ${...}
  * sections parsed out. We replace the node by altering node.parentNode but
  * we could probably use a DOM Text API to achieve the same thing.
  * @param node The Text node to work on
  * @param data The data to use in calls to envEval()
  */
 function processTextNode(state, node, data) {
   // Replace references in other attributes
-  var value = node.data;
+  let value = node.data;
   // We can't use the string.replace() with function trick (see generic
   // attribute processing in processNode()) because we need to support
   // functions that return DOM nodes, so we can't have the conversion to a
   // string.
   // Instead we process the string as an array of parts. In order to split
   // the string up, we first replace '${' with '\uF001$' and '}' with '\uF002'
   // We can then split using \uF001 or \uF002 to get an array of strings
   // where scripts are prefixed with $.
   // \uF001 and \uF002 are just unicode chars reserved for private use.
-  value = value.replace(TEMPLATE_REGION, '\uF001$$$1\uF002');
+  value = value.replace(TEMPLATE_REGION, "\uF001$$$1\uF002");
   // Split a string using the unicode chars F001 and F002.
-  var parts = value.split(/\uF001|\uF002/);
+  let parts = value.split(/\uF001|\uF002/);
   if (parts.length > 1) {
-    parts.forEach(function(part) {
-      if (part === null || part === undefined || part === '') {
+    parts.forEach(function (part) {
+      if (part === null || part === undefined || part === "") {
         return;
       }
-      if (part.charAt(0) === '$') {
+      if (part.charAt(0) === "$") {
         part = envEval(state, part.slice(1), data, node.data);
       }
-      var cState = cloneState(state);
-      handleAsync(part, node, function(reply, siblingNode) {
-        var doc = siblingNode.ownerDocument;
+      let cState = cloneState(state);
+      handleAsync(part, node, function (reply, siblingNode) {
+        let doc = siblingNode.ownerDocument;
         if (reply == null) {
-          reply = cState.options.blankNullUndefined ? '' : '' + reply;
+          reply = cState.options.blankNullUndefined ? "" : "" + reply;
         }
-        if (typeof reply.cloneNode === 'function') {
+        if (typeof reply.cloneNode === "function") {
           // i.e. if (reply instanceof Element) { ...
           reply = maybeImportNode(cState, reply, doc);
           siblingNode.parentNode.insertBefore(reply, siblingNode);
-        }
-        else if (typeof reply.item === 'function' && reply.length) {
+        } else if (typeof reply.item === "function" && reply.length) {
           // NodeLists can be live, in which case maybeImportNode can
           // remove them from the document, and thus the NodeList, which in
           // turn breaks iteration. So first we clone the list
-          var list = Array.prototype.slice.call(reply, 0);
-          list.forEach(function(child) {
-            var imported = maybeImportNode(cState, child, doc);
+          let list = Array.prototype.slice.call(reply, 0);
+          list.forEach(function (child) {
+            let imported = maybeImportNode(cState, child, doc);
             siblingNode.parentNode.insertBefore(imported, siblingNode);
           });
-        }
-        else {
+        } else {
           // if thing isn't a DOM element then wrap its string value in one
           reply = doc.createTextNode(reply.toString());
           siblingNode.parentNode.insertBefore(reply, siblingNode);
         }
       });
     });
     node.parentNode.removeChild(node);
   }
@@ -459,42 +444,41 @@ function maybeImportNode(state, node, do
  * an inserter function.
  * @param thing The object which could be real data or a promise of real data
  * we use it directly if it's not a promise, or resolve it if it is.
  * @param siblingNode The element before which we insert new elements.
  * @param inserter The function to to the insertion. If thing is not a promise
  * then handleAsync() is just 'inserter(thing, siblingNode)'
  */
 function handleAsync(thing, siblingNode, inserter) {
-  if (thing != null && typeof thing.then === 'function') {
+  if (thing != null && typeof thing.then === "function") {
     // Placeholder element to be replaced once we have the real data
-    var tempNode = siblingNode.ownerDocument.createElement('span');
+    let tempNode = siblingNode.ownerDocument.createElement("span");
     siblingNode.parentNode.insertBefore(tempNode, siblingNode);
-    thing.then(function(delayed) {
+    thing.then(function (delayed) {
       inserter(delayed, tempNode);
       if (tempNode.parentNode != null) {
         tempNode.parentNode.removeChild(tempNode);
       }
-    }).then(null, function(error) {
+    }).then(null, function (error) {
       console.error(error.stack);
     });
-  }
-  else {
+  } else {
     inserter(thing, siblingNode);
   }
 }
 
 /**
  * Warn of string does not begin '${' and end '}'
  * @param str the string to check.
  * @return The string stripped of ${ and }, or untouched if it does not match
  */
 function stripBraces(state, str) {
   if (!str.match(TEMPLATE_REGION)) {
-    handleError(state, 'Expected ' + str + ' to match ${...}');
+    handleError(state, "Expected " + str + " to match ${...}");
     return str;
   }
   return str.slice(2, -1);
 }
 
 /**
  * Combined getter and setter that works with a path through some data set.
  * For example:
@@ -509,111 +493,108 @@ function stripBraces(state, str) {
  * @param data the data to use for node processing
  * @param newValue (optional) If defined, this value will replace the
  * original value for the data at the path specified.
  * @return The value pointed to by <tt>path</tt> before any
  * <tt>newValue</tt> is applied.
  */
 function property(state, path, data, newValue) {
   try {
-    if (typeof path === 'string') {
-      path = path.split('.');
+    if (typeof path === "string") {
+      path = path.split(".");
     }
-    var value = data[path[0]];
+    let value = data[path[0]];
     if (path.length === 1) {
       if (newValue !== undefined) {
         data[path[0]] = newValue;
       }
-      if (typeof value === 'function') {
+      if (typeof value === "function") {
         return value.bind(data);
       }
       return value;
     }
     if (!value) {
-      handleError(state, '"' + path[0] + '" is undefined');
+      handleError(state, "\"" + path[0] + "\" is undefined");
       return null;
     }
     return property(state, path.slice(1), value, newValue);
-  }
-  catch (ex) {
-    handleError(state, 'Path error with \'' + path + '\'', ex);
-    return '${' + path + '}';
+  } catch (ex) {
+    handleError(state, "Path error with '" + path + "'", ex);
+    return "${" + path + "}";
   }
 }
 
 /**
  * Like eval, but that creates a context of the variables in <tt>env</tt> in
  * which the script is evaluated.
  * @param script The string to be evaluated.
  * @param data The environment in which to eval the script.
  * @param frame Optional debugging string in case of failure.
  * @return The return value of the script, or the error message if the script
  * execution failed.
  */
 function envEval(state, script, data, frame) {
   try {
-    state.stack.push(frame.replace(/\s+/g, ' '));
+    state.stack.push(frame.replace(/\s+/g, " "));
     // Detect if a script is capable of being interpreted using property()
     if (/^[_a-zA-Z0-9.]*$/.test(script)) {
       return property(state, script, data);
     }
-    else {
-      if (!state.options.allowEval) {
-        handleError(state, 'allowEval is not set, however \'' + script + '\'' +
-            ' can not be resolved using a simple property path.');
-        return '${' + script + '}';
-      }
+    if (!state.options.allowEval) {
+      handleError(state, "allowEval is not set, however '" + script + "'" +
+                  " can not be resolved using a simple property path.");
+      return "${" + script + "}";
+    }
 
-      // What we're looking to do is basically:
-      //   with(data) { return eval(script); }
-      // except in strict mode where 'with' is banned.
-      // So we create a function which has a parameter list the same as the
-      // keys in 'data' and with 'script' as its function body.
-      // We then call this function with the values in 'data'
-      var keys = allKeys(data);
-      var func = Function.apply(null, keys.concat("return " + script));
+    // What we're looking to do is basically:
+    //   with(data) { return eval(script); }
+    // except in strict mode where 'with' is banned.
+    // So we create a function which has a parameter list the same as the
+    // keys in 'data' and with 'script' as its function body.
+    // We then call this function with the values in 'data'
+    let keys = allKeys(data);
+    let func = Function.apply(null, keys.concat("return " + script));
 
-      var values = keys.map(function(key) { return data[key]; });
-      return func.apply(null, values);
+    let values = keys.map((key) => data[key]);
+    return func.apply(null, values);
 
-      // TODO: The 'with' method is different from the code above in the value
-      // of 'this' when calling functions. For example:
-      //   envEval(state, 'foo()', { foo: function() { return this; } }, ...);
-      // The global for 'foo' when using 'with' is the data object. However the
-      // code above, the global is null. (Using 'func.apply(data, values)'
-      // changes 'this' in the 'foo()' frame, but not in the inside the body
-      // of 'foo', so that wouldn't help)
-    }
-  }
-  catch (ex) {
-    handleError(state, 'Template error evaluating \'' + script + '\'', ex);
-    return '${' + script + '}';
-  }
-  finally {
+    // TODO: The 'with' method is different from the code above in the value
+    // of 'this' when calling functions. For example:
+    //   envEval(state, 'foo()', { foo: function () { return this; } }, ...);
+    // The global for 'foo' when using 'with' is the data object. However the
+    // code above, the global is null. (Using 'func.apply(data, values)'
+    // changes 'this' in the 'foo()' frame, but not in the inside the body
+    // of 'foo', so that wouldn't help)
+  } catch (ex) {
+    handleError(state, "Template error evaluating '" + script + "'", ex);
+    return "${" + script + "}";
+  } finally {
     state.stack.pop();
   }
 }
 
 /**
  * Object.keys() that respects the prototype chain
  */
 function allKeys(data) {
-  var keys = [];
-  for (var key in data) { keys.push(key); }
+  let keys = [];
+  for (let key in data) {
+    keys.push(key);
+  }
   return keys;
 }
 
 /**
  * A generic way of reporting errors, for easy overloading in different
  * environments.
  * @param message the error message to report.
  * @param ex optional associated exception.
  */
 function handleError(state, message, ex) {
-  logError(message + ' (In: ' + state.stack.join(' > ') + ')');
+  logError(message + " (In: " + state.stack.join(" > ") + ")");
   if (ex) {
     logError(ex);
   }
 }
 
 /**
  * A generic way of reporting errors, for easy overloading in different
  * environments.