1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/devtools/gcli/Templater.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,599 @@ 1.4 +/* 1.5 + * Copyright 2012, Mozilla Foundation and contributors 1.6 + * 1.7 + * Licensed under the Apache License, Version 2.0 (the "License"); 1.8 + * you may not use this file except in compliance with the License. 1.9 + * You may obtain a copy of the License at 1.10 + * 1.11 + * http://www.apache.org/licenses/LICENSE-2.0 1.12 + * 1.13 + * Unless required by applicable law or agreed to in writing, software 1.14 + * distributed under the License is distributed on an "AS IS" BASIS, 1.15 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1.16 + * See the License for the specific language governing permissions and 1.17 + * limitations under the License. 1.18 + */ 1.19 + 1.20 +this.EXPORTED_SYMBOLS = [ "template" ]; 1.21 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); 1.22 +XPCOMUtils.defineLazyModuleGetter(this, "console", 1.23 + "resource://gre/modules/devtools/Console.jsm"); 1.24 + 1.25 +'do not use strict'; 1.26 + 1.27 +// WARNING: do not 'use strict' without reading the notes in envEval(); 1.28 +// Also don't remove the 'do not use strict' marker. The orion build uses these 1.29 +// markers to know where to insert AMD headers. 1.30 + 1.31 +/** 1.32 + * For full documentation, see: 1.33 + * https://github.com/mozilla/domtemplate/blob/master/README.md 1.34 + */ 1.35 + 1.36 +/** 1.37 + * Begin a new templating process. 1.38 + * @param node A DOM element or string referring to an element's id 1.39 + * @param data Data to use in filling out the template 1.40 + * @param options Options to customize the template processing. One of: 1.41 + * - allowEval: boolean (default false) Basic template interpolations are 1.42 + * either property paths (e.g. ${a.b.c.d}), or if allowEval=true then we 1.43 + * allow arbitrary JavaScript 1.44 + * - stack: string or array of strings (default empty array) The template 1.45 + * engine maintains a stack of tasks to help debug where it is. This allows 1.46 + * this stack to be prefixed with a template name 1.47 + * - blankNullUndefined: By default DOMTemplate exports null and undefined 1.48 + * values using the strings 'null' and 'undefined', which can be helpful for 1.49 + * debugging, but can introduce unnecessary extra logic in a template to 1.50 + * convert null/undefined to ''. By setting blankNullUndefined:true, this 1.51 + * conversion is handled by DOMTemplate 1.52 + */ 1.53 +var template = function(node, data, options) { 1.54 + var state = { 1.55 + options: options || {}, 1.56 + // We keep a track of the nodes that we've passed through so we can keep 1.57 + // data.__element pointing to the correct node 1.58 + nodes: [] 1.59 + }; 1.60 + 1.61 + state.stack = state.options.stack; 1.62 + 1.63 + if (!Array.isArray(state.stack)) { 1.64 + if (typeof state.stack === 'string') { 1.65 + state.stack = [ options.stack ]; 1.66 + } 1.67 + else { 1.68 + state.stack = []; 1.69 + } 1.70 + } 1.71 + 1.72 + processNode(state, node, data); 1.73 +}; 1.74 + 1.75 +// 1.76 +// 1.77 +// 1.78 + 1.79 +/** 1.80 + * Helper for the places where we need to act asynchronously and keep track of 1.81 + * where we are right now 1.82 + */ 1.83 +function cloneState(state) { 1.84 + return { 1.85 + options: state.options, 1.86 + stack: state.stack.slice(), 1.87 + nodes: state.nodes.slice() 1.88 + }; 1.89 +} 1.90 + 1.91 +/** 1.92 + * Regex used to find ${...} sections in some text. 1.93 + * Performance note: This regex uses ( and ) to capture the 'script' for 1.94 + * further processing. Not all of the uses of this regex use this feature so 1.95 + * if use of the capturing group is a performance drain then we should split 1.96 + * this regex in two. 1.97 + */ 1.98 +var TEMPLATE_REGION = /\$\{([^}]*)\}/g; 1.99 + 1.100 +/** 1.101 + * Recursive function to walk the tree processing the attributes as it goes. 1.102 + * @param node the node to process. If you pass a string in instead of a DOM 1.103 + * element, it is assumed to be an id for use with document.getElementById() 1.104 + * @param data the data to use for node processing. 1.105 + */ 1.106 +function processNode(state, node, data) { 1.107 + if (typeof node === 'string') { 1.108 + node = document.getElementById(node); 1.109 + } 1.110 + if (data == null) { 1.111 + data = {}; 1.112 + } 1.113 + state.stack.push(node.nodeName + (node.id ? '#' + node.id : '')); 1.114 + var pushedNode = false; 1.115 + try { 1.116 + // Process attributes 1.117 + if (node.attributes && node.attributes.length) { 1.118 + // We need to handle 'foreach' and 'if' first because they might stop 1.119 + // some types of processing from happening, and foreach must come first 1.120 + // because it defines new data on which 'if' might depend. 1.121 + if (node.hasAttribute('foreach')) { 1.122 + processForEach(state, node, data); 1.123 + return; 1.124 + } 1.125 + if (node.hasAttribute('if')) { 1.126 + if (!processIf(state, node, data)) { 1.127 + return; 1.128 + } 1.129 + } 1.130 + // Only make the node available once we know it's not going away 1.131 + state.nodes.push(data.__element); 1.132 + data.__element = node; 1.133 + pushedNode = true; 1.134 + // It's good to clean up the attributes when we've processed them, 1.135 + // but if we do it straight away, we mess up the array index 1.136 + var attrs = Array.prototype.slice.call(node.attributes); 1.137 + for (var i = 0; i < attrs.length; i++) { 1.138 + var value = attrs[i].value; 1.139 + var name = attrs[i].name; 1.140 + 1.141 + state.stack.push(name); 1.142 + try { 1.143 + if (name === 'save') { 1.144 + // Save attributes are a setter using the node 1.145 + value = stripBraces(state, value); 1.146 + property(state, value, data, node); 1.147 + node.removeAttribute('save'); 1.148 + } 1.149 + else if (name.substring(0, 2) === 'on') { 1.150 + // If this attribute value contains only an expression 1.151 + if (value.substring(0, 2) === '${' && value.slice(-1) === '}' && 1.152 + value.indexOf('${', 2) === -1) { 1.153 + value = stripBraces(state, value); 1.154 + var func = property(state, value, data); 1.155 + if (typeof func === 'function') { 1.156 + node.removeAttribute(name); 1.157 + var capture = node.hasAttribute('capture' + name.substring(2)); 1.158 + node.addEventListener(name.substring(2), func, capture); 1.159 + if (capture) { 1.160 + node.removeAttribute('capture' + name.substring(2)); 1.161 + } 1.162 + } 1.163 + else { 1.164 + // Attribute value is not a function - use as a DOM-L0 string 1.165 + node.setAttribute(name, func); 1.166 + } 1.167 + } 1.168 + else { 1.169 + // Attribute value is not a single expression use as DOM-L0 1.170 + node.setAttribute(name, processString(state, value, data)); 1.171 + } 1.172 + } 1.173 + else { 1.174 + node.removeAttribute(name); 1.175 + // Remove '_' prefix of attribute names so the DOM won't try 1.176 + // to use them before we've processed the template 1.177 + if (name.charAt(0) === '_') { 1.178 + name = name.substring(1); 1.179 + } 1.180 + 1.181 + // Async attributes can only work if the whole attribute is async 1.182 + var replacement; 1.183 + if (value.indexOf('${') === 0 && 1.184 + value.charAt(value.length - 1) === '}') { 1.185 + replacement = envEval(state, value.slice(2, -1), data, value); 1.186 + if (replacement && typeof replacement.then === 'function') { 1.187 + node.setAttribute(name, ''); 1.188 + replacement.then(function(newValue) { 1.189 + node.setAttribute(name, newValue); 1.190 + }).then(null, console.error); 1.191 + } 1.192 + else { 1.193 + if (state.options.blankNullUndefined && replacement == null) { 1.194 + replacement = ''; 1.195 + } 1.196 + node.setAttribute(name, replacement); 1.197 + } 1.198 + } 1.199 + else { 1.200 + node.setAttribute(name, processString(state, value, data)); 1.201 + } 1.202 + } 1.203 + } 1.204 + finally { 1.205 + state.stack.pop(); 1.206 + } 1.207 + } 1.208 + } 1.209 + 1.210 + // Loop through our children calling processNode. First clone them, so the 1.211 + // set of nodes that we visit will be unaffected by additions or removals. 1.212 + var childNodes = Array.prototype.slice.call(node.childNodes); 1.213 + for (var j = 0; j < childNodes.length; j++) { 1.214 + processNode(state, childNodes[j], data); 1.215 + } 1.216 + 1.217 + if (node.nodeType === 3 /*Node.TEXT_NODE*/) { 1.218 + processTextNode(state, node, data); 1.219 + } 1.220 + } 1.221 + finally { 1.222 + if (pushedNode) { 1.223 + data.__element = state.nodes.pop(); 1.224 + } 1.225 + state.stack.pop(); 1.226 + } 1.227 +} 1.228 + 1.229 +/** 1.230 + * Handle attribute values where the output can only be a string 1.231 + */ 1.232 +function processString(state, value, data) { 1.233 + return value.replace(TEMPLATE_REGION, function(path) { 1.234 + var insert = envEval(state, path.slice(2, -1), data, value); 1.235 + return state.options.blankNullUndefined && insert == null ? '' : insert; 1.236 + }); 1.237 +} 1.238 + 1.239 +/** 1.240 + * Handle <x if="${...}"> 1.241 + * @param node An element with an 'if' attribute 1.242 + * @param data The data to use with envEval() 1.243 + * @returns true if processing should continue, false otherwise 1.244 + */ 1.245 +function processIf(state, node, data) { 1.246 + state.stack.push('if'); 1.247 + try { 1.248 + var originalValue = node.getAttribute('if'); 1.249 + var value = stripBraces(state, originalValue); 1.250 + var recurse = true; 1.251 + try { 1.252 + var reply = envEval(state, value, data, originalValue); 1.253 + recurse = !!reply; 1.254 + } 1.255 + catch (ex) { 1.256 + handleError(state, 'Error with \'' + value + '\'', ex); 1.257 + recurse = false; 1.258 + } 1.259 + if (!recurse) { 1.260 + node.parentNode.removeChild(node); 1.261 + } 1.262 + node.removeAttribute('if'); 1.263 + return recurse; 1.264 + } 1.265 + finally { 1.266 + state.stack.pop(); 1.267 + } 1.268 +} 1.269 + 1.270 +/** 1.271 + * Handle <x foreach="param in ${array}"> and the special case of 1.272 + * <loop foreach="param in ${array}">. 1.273 + * This function is responsible for extracting what it has to do from the 1.274 + * attributes, and getting the data to work on (including resolving promises 1.275 + * in getting the array). It delegates to processForEachLoop to actually 1.276 + * unroll the data. 1.277 + * @param node An element with a 'foreach' attribute 1.278 + * @param data The data to use with envEval() 1.279 + */ 1.280 +function processForEach(state, node, data) { 1.281 + state.stack.push('foreach'); 1.282 + try { 1.283 + var originalValue = node.getAttribute('foreach'); 1.284 + var value = originalValue; 1.285 + 1.286 + var paramName = 'param'; 1.287 + if (value.charAt(0) === '$') { 1.288 + // No custom loop variable name. Use the default: 'param' 1.289 + value = stripBraces(state, value); 1.290 + } 1.291 + else { 1.292 + // Extract the loop variable name from 'NAME in ${ARRAY}' 1.293 + var nameArr = value.split(' in '); 1.294 + paramName = nameArr[0].trim(); 1.295 + value = stripBraces(state, nameArr[1].trim()); 1.296 + } 1.297 + node.removeAttribute('foreach'); 1.298 + try { 1.299 + var evaled = envEval(state, value, data, originalValue); 1.300 + var cState = cloneState(state); 1.301 + handleAsync(evaled, node, function(reply, siblingNode) { 1.302 + processForEachLoop(cState, reply, node, siblingNode, data, paramName); 1.303 + }); 1.304 + node.parentNode.removeChild(node); 1.305 + } 1.306 + catch (ex) { 1.307 + handleError(state, 'Error with \'' + value + '\'', ex); 1.308 + } 1.309 + } 1.310 + finally { 1.311 + state.stack.pop(); 1.312 + } 1.313 +} 1.314 + 1.315 +/** 1.316 + * Called by processForEach to handle looping over the data in a foreach loop. 1.317 + * This works with both arrays and objects. 1.318 + * Calls processForEachMember() for each member of 'set' 1.319 + * @param set The object containing the data to loop over 1.320 + * @param templNode The node to copy for each set member 1.321 + * @param sibling The sibling node to which we add things 1.322 + * @param data the data to use for node processing 1.323 + * @param paramName foreach loops have a name for the parameter currently being 1.324 + * processed. The default is 'param'. e.g. <loop foreach="param in ${x}">... 1.325 + */ 1.326 +function processForEachLoop(state, set, templNode, sibling, data, paramName) { 1.327 + if (Array.isArray(set)) { 1.328 + set.forEach(function(member, i) { 1.329 + processForEachMember(state, member, templNode, sibling, 1.330 + data, paramName, '' + i); 1.331 + }); 1.332 + } 1.333 + else { 1.334 + for (var member in set) { 1.335 + if (set.hasOwnProperty(member)) { 1.336 + processForEachMember(state, member, templNode, sibling, 1.337 + data, paramName, member); 1.338 + } 1.339 + } 1.340 + } 1.341 +} 1.342 + 1.343 +/** 1.344 + * Called by processForEachLoop() to resolve any promises in the array (the 1.345 + * array itself can also be a promise, but that is resolved by 1.346 + * processForEach()). Handle <LOOP> elements (which are taken out of the DOM), 1.347 + * clone the template node, and pass the processing on to processNode(). 1.348 + * @param member The data item to use in templating 1.349 + * @param templNode The node to copy for each set member 1.350 + * @param siblingNode The parent node to which we add things 1.351 + * @param data the data to use for node processing 1.352 + * @param paramName The name given to 'member' by the foreach attribute 1.353 + * @param frame A name to push on the stack for debugging 1.354 + */ 1.355 +function processForEachMember(state, member, templNode, siblingNode, data, paramName, frame) { 1.356 + state.stack.push(frame); 1.357 + try { 1.358 + var cState = cloneState(state); 1.359 + handleAsync(member, siblingNode, function(reply, node) { 1.360 + data[paramName] = reply; 1.361 + if (node.parentNode != null) { 1.362 + if (templNode.nodeName.toLowerCase() === 'loop') { 1.363 + for (var i = 0; i < templNode.childNodes.length; i++) { 1.364 + var clone = templNode.childNodes[i].cloneNode(true); 1.365 + node.parentNode.insertBefore(clone, node); 1.366 + processNode(cState, clone, data); 1.367 + } 1.368 + } 1.369 + else { 1.370 + var clone = templNode.cloneNode(true); 1.371 + clone.removeAttribute('foreach'); 1.372 + node.parentNode.insertBefore(clone, node); 1.373 + processNode(cState, clone, data); 1.374 + } 1.375 + } 1.376 + delete data[paramName]; 1.377 + }); 1.378 + } 1.379 + finally { 1.380 + state.stack.pop(); 1.381 + } 1.382 +} 1.383 + 1.384 +/** 1.385 + * Take a text node and replace it with another text node with the ${...} 1.386 + * sections parsed out. We replace the node by altering node.parentNode but 1.387 + * we could probably use a DOM Text API to achieve the same thing. 1.388 + * @param node The Text node to work on 1.389 + * @param data The data to use in calls to envEval() 1.390 + */ 1.391 +function processTextNode(state, node, data) { 1.392 + // Replace references in other attributes 1.393 + var value = node.data; 1.394 + // We can't use the string.replace() with function trick (see generic 1.395 + // attribute processing in processNode()) because we need to support 1.396 + // functions that return DOM nodes, so we can't have the conversion to a 1.397 + // string. 1.398 + // Instead we process the string as an array of parts. In order to split 1.399 + // the string up, we first replace '${' with '\uF001$' and '}' with '\uF002' 1.400 + // We can then split using \uF001 or \uF002 to get an array of strings 1.401 + // where scripts are prefixed with $. 1.402 + // \uF001 and \uF002 are just unicode chars reserved for private use. 1.403 + value = value.replace(TEMPLATE_REGION, '\uF001$$$1\uF002'); 1.404 + // Split a string using the unicode chars F001 and F002. 1.405 + var parts = value.split(/\uF001|\uF002/); 1.406 + if (parts.length > 1) { 1.407 + parts.forEach(function(part) { 1.408 + if (part === null || part === undefined || part === '') { 1.409 + return; 1.410 + } 1.411 + if (part.charAt(0) === '$') { 1.412 + part = envEval(state, part.slice(1), data, node.data); 1.413 + } 1.414 + var cState = cloneState(state); 1.415 + handleAsync(part, node, function(reply, siblingNode) { 1.416 + var doc = siblingNode.ownerDocument; 1.417 + if (reply == null) { 1.418 + reply = cState.options.blankNullUndefined ? '' : '' + reply; 1.419 + } 1.420 + if (typeof reply.cloneNode === 'function') { 1.421 + // i.e. if (reply instanceof Element) { ... 1.422 + reply = maybeImportNode(cState, reply, doc); 1.423 + siblingNode.parentNode.insertBefore(reply, siblingNode); 1.424 + } 1.425 + else if (typeof reply.item === 'function' && reply.length) { 1.426 + // NodeLists can be live, in which case maybeImportNode can 1.427 + // remove them from the document, and thus the NodeList, which in 1.428 + // turn breaks iteration. So first we clone the list 1.429 + var list = Array.prototype.slice.call(reply, 0); 1.430 + list.forEach(function(child) { 1.431 + var imported = maybeImportNode(cState, child, doc); 1.432 + siblingNode.parentNode.insertBefore(imported, siblingNode); 1.433 + }); 1.434 + } 1.435 + else { 1.436 + // if thing isn't a DOM element then wrap its string value in one 1.437 + reply = doc.createTextNode(reply.toString()); 1.438 + siblingNode.parentNode.insertBefore(reply, siblingNode); 1.439 + } 1.440 + }); 1.441 + }); 1.442 + node.parentNode.removeChild(node); 1.443 + } 1.444 +} 1.445 + 1.446 +/** 1.447 + * Return node or a import of node, if it's not in the given document 1.448 + * @param node The node that we want to be properly owned 1.449 + * @param doc The document that the given node should belong to 1.450 + * @return A node that belongs to the given document 1.451 + */ 1.452 +function maybeImportNode(state, node, doc) { 1.453 + return node.ownerDocument === doc ? node : doc.importNode(node, true); 1.454 +} 1.455 + 1.456 +/** 1.457 + * A function to handle the fact that some nodes can be promises, so we check 1.458 + * and resolve if needed using a marker node to keep our place before calling 1.459 + * an inserter function. 1.460 + * @param thing The object which could be real data or a promise of real data 1.461 + * we use it directly if it's not a promise, or resolve it if it is. 1.462 + * @param siblingNode The element before which we insert new elements. 1.463 + * @param inserter The function to to the insertion. If thing is not a promise 1.464 + * then handleAsync() is just 'inserter(thing, siblingNode)' 1.465 + */ 1.466 +function handleAsync(thing, siblingNode, inserter) { 1.467 + if (thing != null && typeof thing.then === 'function') { 1.468 + // Placeholder element to be replaced once we have the real data 1.469 + var tempNode = siblingNode.ownerDocument.createElement('span'); 1.470 + siblingNode.parentNode.insertBefore(tempNode, siblingNode); 1.471 + thing.then(function(delayed) { 1.472 + inserter(delayed, tempNode); 1.473 + if (tempNode.parentNode != null) { 1.474 + tempNode.parentNode.removeChild(tempNode); 1.475 + } 1.476 + }).then(null, function(error) { 1.477 + console.error(error.stack); 1.478 + }); 1.479 + } 1.480 + else { 1.481 + inserter(thing, siblingNode); 1.482 + } 1.483 +} 1.484 + 1.485 +/** 1.486 + * Warn of string does not begin '${' and end '}' 1.487 + * @param str the string to check. 1.488 + * @return The string stripped of ${ and }, or untouched if it does not match 1.489 + */ 1.490 +function stripBraces(state, str) { 1.491 + if (!str.match(TEMPLATE_REGION)) { 1.492 + handleError(state, 'Expected ' + str + ' to match ${...}'); 1.493 + return str; 1.494 + } 1.495 + return str.slice(2, -1); 1.496 +} 1.497 + 1.498 +/** 1.499 + * Combined getter and setter that works with a path through some data set. 1.500 + * For example: 1.501 + * <ul> 1.502 + * <li>property(state, 'a.b', { a: { b: 99 }}); // returns 99 1.503 + * <li>property(state, 'a', { a: { b: 99 }}); // returns { b: 99 } 1.504 + * <li>property(state, 'a', { a: { b: 99 }}, 42); // returns 99 and alters the 1.505 + * input data to be { a: { b: 42 }} 1.506 + * </ul> 1.507 + * @param path An array of strings indicating the path through the data, or 1.508 + * a string to be cut into an array using <tt>split('.')</tt> 1.509 + * @param data the data to use for node processing 1.510 + * @param newValue (optional) If defined, this value will replace the 1.511 + * original value for the data at the path specified. 1.512 + * @return The value pointed to by <tt>path</tt> before any 1.513 + * <tt>newValue</tt> is applied. 1.514 + */ 1.515 +function property(state, path, data, newValue) { 1.516 + try { 1.517 + if (typeof path === 'string') { 1.518 + path = path.split('.'); 1.519 + } 1.520 + var value = data[path[0]]; 1.521 + if (path.length === 1) { 1.522 + if (newValue !== undefined) { 1.523 + data[path[0]] = newValue; 1.524 + } 1.525 + if (typeof value === 'function') { 1.526 + return value.bind(data); 1.527 + } 1.528 + return value; 1.529 + } 1.530 + if (!value) { 1.531 + handleError(state, '"' + path[0] + '" is undefined'); 1.532 + return null; 1.533 + } 1.534 + return property(state, path.slice(1), value, newValue); 1.535 + } 1.536 + catch (ex) { 1.537 + handleError(state, 'Path error with \'' + path + '\'', ex); 1.538 + return '${' + path + '}'; 1.539 + } 1.540 +} 1.541 + 1.542 +/** 1.543 + * Like eval, but that creates a context of the variables in <tt>env</tt> in 1.544 + * which the script is evaluated. 1.545 + * WARNING: This script uses 'with' which is generally regarded to be evil. 1.546 + * The alternative is to create a Function at runtime that takes X parameters 1.547 + * according to the X keys in the env object, and then call that function using 1.548 + * the values in the env object. This is likely to be slow, but workable. 1.549 + * @param script The string to be evaluated. 1.550 + * @param data The environment in which to eval the script. 1.551 + * @param frame Optional debugging string in case of failure. 1.552 + * @return The return value of the script, or the error message if the script 1.553 + * execution failed. 1.554 + */ 1.555 +function envEval(state, script, data, frame) { 1.556 + try { 1.557 + state.stack.push(frame.replace(/\s+/g, ' ')); 1.558 + // Detect if a script is capable of being interpreted using property() 1.559 + if (/^[_a-zA-Z0-9.]*$/.test(script)) { 1.560 + return property(state, script, data); 1.561 + } 1.562 + else { 1.563 + if (!state.options.allowEval) { 1.564 + handleError(state, 'allowEval is not set, however \'' + script + '\'' + 1.565 + ' can not be resolved using a simple property path.'); 1.566 + return '${' + script + '}'; 1.567 + } 1.568 + with (data) { 1.569 + return eval(script); 1.570 + } 1.571 + } 1.572 + } 1.573 + catch (ex) { 1.574 + handleError(state, 'Template error evaluating \'' + script + '\'', ex); 1.575 + return '${' + script + '}'; 1.576 + } 1.577 + finally { 1.578 + state.stack.pop(); 1.579 + } 1.580 +} 1.581 + 1.582 +/** 1.583 + * A generic way of reporting errors, for easy overloading in different 1.584 + * environments. 1.585 + * @param message the error message to report. 1.586 + * @param ex optional associated exception. 1.587 + */ 1.588 +function handleError(state, message, ex) { 1.589 + logError(message + ' (In: ' + state.stack.join(' > ') + ')'); 1.590 + if (ex) { 1.591 + logError(ex); 1.592 + } 1.593 +} 1.594 + 1.595 +/** 1.596 + * A generic way of reporting errors, for easy overloading in different 1.597 + * environments. 1.598 + * @param message the error message to report. 1.599 + */ 1.600 +function logError(message) { 1.601 + console.log(message); 1.602 +}