toolkit/devtools/gcli/Templater.jsm

changeset 0
6474c204b198
     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 +}

mercurial