toolkit/devtools/gcli/Templater.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /*
michael@0 2 * Copyright 2012, Mozilla Foundation and contributors
michael@0 3 *
michael@0 4 * Licensed under the Apache License, Version 2.0 (the "License");
michael@0 5 * you may not use this file except in compliance with the License.
michael@0 6 * You may obtain a copy of the License at
michael@0 7 *
michael@0 8 * http://www.apache.org/licenses/LICENSE-2.0
michael@0 9 *
michael@0 10 * Unless required by applicable law or agreed to in writing, software
michael@0 11 * distributed under the License is distributed on an "AS IS" BASIS,
michael@0 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
michael@0 13 * See the License for the specific language governing permissions and
michael@0 14 * limitations under the License.
michael@0 15 */
michael@0 16
michael@0 17 this.EXPORTED_SYMBOLS = [ "template" ];
michael@0 18 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 19 XPCOMUtils.defineLazyModuleGetter(this, "console",
michael@0 20 "resource://gre/modules/devtools/Console.jsm");
michael@0 21
michael@0 22 'do not use strict';
michael@0 23
michael@0 24 // WARNING: do not 'use strict' without reading the notes in envEval();
michael@0 25 // Also don't remove the 'do not use strict' marker. The orion build uses these
michael@0 26 // markers to know where to insert AMD headers.
michael@0 27
michael@0 28 /**
michael@0 29 * For full documentation, see:
michael@0 30 * https://github.com/mozilla/domtemplate/blob/master/README.md
michael@0 31 */
michael@0 32
michael@0 33 /**
michael@0 34 * Begin a new templating process.
michael@0 35 * @param node A DOM element or string referring to an element's id
michael@0 36 * @param data Data to use in filling out the template
michael@0 37 * @param options Options to customize the template processing. One of:
michael@0 38 * - allowEval: boolean (default false) Basic template interpolations are
michael@0 39 * either property paths (e.g. ${a.b.c.d}), or if allowEval=true then we
michael@0 40 * allow arbitrary JavaScript
michael@0 41 * - stack: string or array of strings (default empty array) The template
michael@0 42 * engine maintains a stack of tasks to help debug where it is. This allows
michael@0 43 * this stack to be prefixed with a template name
michael@0 44 * - blankNullUndefined: By default DOMTemplate exports null and undefined
michael@0 45 * values using the strings 'null' and 'undefined', which can be helpful for
michael@0 46 * debugging, but can introduce unnecessary extra logic in a template to
michael@0 47 * convert null/undefined to ''. By setting blankNullUndefined:true, this
michael@0 48 * conversion is handled by DOMTemplate
michael@0 49 */
michael@0 50 var template = function(node, data, options) {
michael@0 51 var state = {
michael@0 52 options: options || {},
michael@0 53 // We keep a track of the nodes that we've passed through so we can keep
michael@0 54 // data.__element pointing to the correct node
michael@0 55 nodes: []
michael@0 56 };
michael@0 57
michael@0 58 state.stack = state.options.stack;
michael@0 59
michael@0 60 if (!Array.isArray(state.stack)) {
michael@0 61 if (typeof state.stack === 'string') {
michael@0 62 state.stack = [ options.stack ];
michael@0 63 }
michael@0 64 else {
michael@0 65 state.stack = [];
michael@0 66 }
michael@0 67 }
michael@0 68
michael@0 69 processNode(state, node, data);
michael@0 70 };
michael@0 71
michael@0 72 //
michael@0 73 //
michael@0 74 //
michael@0 75
michael@0 76 /**
michael@0 77 * Helper for the places where we need to act asynchronously and keep track of
michael@0 78 * where we are right now
michael@0 79 */
michael@0 80 function cloneState(state) {
michael@0 81 return {
michael@0 82 options: state.options,
michael@0 83 stack: state.stack.slice(),
michael@0 84 nodes: state.nodes.slice()
michael@0 85 };
michael@0 86 }
michael@0 87
michael@0 88 /**
michael@0 89 * Regex used to find ${...} sections in some text.
michael@0 90 * Performance note: This regex uses ( and ) to capture the 'script' for
michael@0 91 * further processing. Not all of the uses of this regex use this feature so
michael@0 92 * if use of the capturing group is a performance drain then we should split
michael@0 93 * this regex in two.
michael@0 94 */
michael@0 95 var TEMPLATE_REGION = /\$\{([^}]*)\}/g;
michael@0 96
michael@0 97 /**
michael@0 98 * Recursive function to walk the tree processing the attributes as it goes.
michael@0 99 * @param node the node to process. If you pass a string in instead of a DOM
michael@0 100 * element, it is assumed to be an id for use with document.getElementById()
michael@0 101 * @param data the data to use for node processing.
michael@0 102 */
michael@0 103 function processNode(state, node, data) {
michael@0 104 if (typeof node === 'string') {
michael@0 105 node = document.getElementById(node);
michael@0 106 }
michael@0 107 if (data == null) {
michael@0 108 data = {};
michael@0 109 }
michael@0 110 state.stack.push(node.nodeName + (node.id ? '#' + node.id : ''));
michael@0 111 var pushedNode = false;
michael@0 112 try {
michael@0 113 // Process attributes
michael@0 114 if (node.attributes && node.attributes.length) {
michael@0 115 // We need to handle 'foreach' and 'if' first because they might stop
michael@0 116 // some types of processing from happening, and foreach must come first
michael@0 117 // because it defines new data on which 'if' might depend.
michael@0 118 if (node.hasAttribute('foreach')) {
michael@0 119 processForEach(state, node, data);
michael@0 120 return;
michael@0 121 }
michael@0 122 if (node.hasAttribute('if')) {
michael@0 123 if (!processIf(state, node, data)) {
michael@0 124 return;
michael@0 125 }
michael@0 126 }
michael@0 127 // Only make the node available once we know it's not going away
michael@0 128 state.nodes.push(data.__element);
michael@0 129 data.__element = node;
michael@0 130 pushedNode = true;
michael@0 131 // It's good to clean up the attributes when we've processed them,
michael@0 132 // but if we do it straight away, we mess up the array index
michael@0 133 var attrs = Array.prototype.slice.call(node.attributes);
michael@0 134 for (var i = 0; i < attrs.length; i++) {
michael@0 135 var value = attrs[i].value;
michael@0 136 var name = attrs[i].name;
michael@0 137
michael@0 138 state.stack.push(name);
michael@0 139 try {
michael@0 140 if (name === 'save') {
michael@0 141 // Save attributes are a setter using the node
michael@0 142 value = stripBraces(state, value);
michael@0 143 property(state, value, data, node);
michael@0 144 node.removeAttribute('save');
michael@0 145 }
michael@0 146 else if (name.substring(0, 2) === 'on') {
michael@0 147 // If this attribute value contains only an expression
michael@0 148 if (value.substring(0, 2) === '${' && value.slice(-1) === '}' &&
michael@0 149 value.indexOf('${', 2) === -1) {
michael@0 150 value = stripBraces(state, value);
michael@0 151 var func = property(state, value, data);
michael@0 152 if (typeof func === 'function') {
michael@0 153 node.removeAttribute(name);
michael@0 154 var capture = node.hasAttribute('capture' + name.substring(2));
michael@0 155 node.addEventListener(name.substring(2), func, capture);
michael@0 156 if (capture) {
michael@0 157 node.removeAttribute('capture' + name.substring(2));
michael@0 158 }
michael@0 159 }
michael@0 160 else {
michael@0 161 // Attribute value is not a function - use as a DOM-L0 string
michael@0 162 node.setAttribute(name, func);
michael@0 163 }
michael@0 164 }
michael@0 165 else {
michael@0 166 // Attribute value is not a single expression use as DOM-L0
michael@0 167 node.setAttribute(name, processString(state, value, data));
michael@0 168 }
michael@0 169 }
michael@0 170 else {
michael@0 171 node.removeAttribute(name);
michael@0 172 // Remove '_' prefix of attribute names so the DOM won't try
michael@0 173 // to use them before we've processed the template
michael@0 174 if (name.charAt(0) === '_') {
michael@0 175 name = name.substring(1);
michael@0 176 }
michael@0 177
michael@0 178 // Async attributes can only work if the whole attribute is async
michael@0 179 var replacement;
michael@0 180 if (value.indexOf('${') === 0 &&
michael@0 181 value.charAt(value.length - 1) === '}') {
michael@0 182 replacement = envEval(state, value.slice(2, -1), data, value);
michael@0 183 if (replacement && typeof replacement.then === 'function') {
michael@0 184 node.setAttribute(name, '');
michael@0 185 replacement.then(function(newValue) {
michael@0 186 node.setAttribute(name, newValue);
michael@0 187 }).then(null, console.error);
michael@0 188 }
michael@0 189 else {
michael@0 190 if (state.options.blankNullUndefined && replacement == null) {
michael@0 191 replacement = '';
michael@0 192 }
michael@0 193 node.setAttribute(name, replacement);
michael@0 194 }
michael@0 195 }
michael@0 196 else {
michael@0 197 node.setAttribute(name, processString(state, value, data));
michael@0 198 }
michael@0 199 }
michael@0 200 }
michael@0 201 finally {
michael@0 202 state.stack.pop();
michael@0 203 }
michael@0 204 }
michael@0 205 }
michael@0 206
michael@0 207 // Loop through our children calling processNode. First clone them, so the
michael@0 208 // set of nodes that we visit will be unaffected by additions or removals.
michael@0 209 var childNodes = Array.prototype.slice.call(node.childNodes);
michael@0 210 for (var j = 0; j < childNodes.length; j++) {
michael@0 211 processNode(state, childNodes[j], data);
michael@0 212 }
michael@0 213
michael@0 214 if (node.nodeType === 3 /*Node.TEXT_NODE*/) {
michael@0 215 processTextNode(state, node, data);
michael@0 216 }
michael@0 217 }
michael@0 218 finally {
michael@0 219 if (pushedNode) {
michael@0 220 data.__element = state.nodes.pop();
michael@0 221 }
michael@0 222 state.stack.pop();
michael@0 223 }
michael@0 224 }
michael@0 225
michael@0 226 /**
michael@0 227 * Handle attribute values where the output can only be a string
michael@0 228 */
michael@0 229 function processString(state, value, data) {
michael@0 230 return value.replace(TEMPLATE_REGION, function(path) {
michael@0 231 var insert = envEval(state, path.slice(2, -1), data, value);
michael@0 232 return state.options.blankNullUndefined && insert == null ? '' : insert;
michael@0 233 });
michael@0 234 }
michael@0 235
michael@0 236 /**
michael@0 237 * Handle <x if="${...}">
michael@0 238 * @param node An element with an 'if' attribute
michael@0 239 * @param data The data to use with envEval()
michael@0 240 * @returns true if processing should continue, false otherwise
michael@0 241 */
michael@0 242 function processIf(state, node, data) {
michael@0 243 state.stack.push('if');
michael@0 244 try {
michael@0 245 var originalValue = node.getAttribute('if');
michael@0 246 var value = stripBraces(state, originalValue);
michael@0 247 var recurse = true;
michael@0 248 try {
michael@0 249 var reply = envEval(state, value, data, originalValue);
michael@0 250 recurse = !!reply;
michael@0 251 }
michael@0 252 catch (ex) {
michael@0 253 handleError(state, 'Error with \'' + value + '\'', ex);
michael@0 254 recurse = false;
michael@0 255 }
michael@0 256 if (!recurse) {
michael@0 257 node.parentNode.removeChild(node);
michael@0 258 }
michael@0 259 node.removeAttribute('if');
michael@0 260 return recurse;
michael@0 261 }
michael@0 262 finally {
michael@0 263 state.stack.pop();
michael@0 264 }
michael@0 265 }
michael@0 266
michael@0 267 /**
michael@0 268 * Handle <x foreach="param in ${array}"> and the special case of
michael@0 269 * <loop foreach="param in ${array}">.
michael@0 270 * This function is responsible for extracting what it has to do from the
michael@0 271 * attributes, and getting the data to work on (including resolving promises
michael@0 272 * in getting the array). It delegates to processForEachLoop to actually
michael@0 273 * unroll the data.
michael@0 274 * @param node An element with a 'foreach' attribute
michael@0 275 * @param data The data to use with envEval()
michael@0 276 */
michael@0 277 function processForEach(state, node, data) {
michael@0 278 state.stack.push('foreach');
michael@0 279 try {
michael@0 280 var originalValue = node.getAttribute('foreach');
michael@0 281 var value = originalValue;
michael@0 282
michael@0 283 var paramName = 'param';
michael@0 284 if (value.charAt(0) === '$') {
michael@0 285 // No custom loop variable name. Use the default: 'param'
michael@0 286 value = stripBraces(state, value);
michael@0 287 }
michael@0 288 else {
michael@0 289 // Extract the loop variable name from 'NAME in ${ARRAY}'
michael@0 290 var nameArr = value.split(' in ');
michael@0 291 paramName = nameArr[0].trim();
michael@0 292 value = stripBraces(state, nameArr[1].trim());
michael@0 293 }
michael@0 294 node.removeAttribute('foreach');
michael@0 295 try {
michael@0 296 var evaled = envEval(state, value, data, originalValue);
michael@0 297 var cState = cloneState(state);
michael@0 298 handleAsync(evaled, node, function(reply, siblingNode) {
michael@0 299 processForEachLoop(cState, reply, node, siblingNode, data, paramName);
michael@0 300 });
michael@0 301 node.parentNode.removeChild(node);
michael@0 302 }
michael@0 303 catch (ex) {
michael@0 304 handleError(state, 'Error with \'' + value + '\'', ex);
michael@0 305 }
michael@0 306 }
michael@0 307 finally {
michael@0 308 state.stack.pop();
michael@0 309 }
michael@0 310 }
michael@0 311
michael@0 312 /**
michael@0 313 * Called by processForEach to handle looping over the data in a foreach loop.
michael@0 314 * This works with both arrays and objects.
michael@0 315 * Calls processForEachMember() for each member of 'set'
michael@0 316 * @param set The object containing the data to loop over
michael@0 317 * @param templNode The node to copy for each set member
michael@0 318 * @param sibling The sibling node to which we add things
michael@0 319 * @param data the data to use for node processing
michael@0 320 * @param paramName foreach loops have a name for the parameter currently being
michael@0 321 * processed. The default is 'param'. e.g. <loop foreach="param in ${x}">...
michael@0 322 */
michael@0 323 function processForEachLoop(state, set, templNode, sibling, data, paramName) {
michael@0 324 if (Array.isArray(set)) {
michael@0 325 set.forEach(function(member, i) {
michael@0 326 processForEachMember(state, member, templNode, sibling,
michael@0 327 data, paramName, '' + i);
michael@0 328 });
michael@0 329 }
michael@0 330 else {
michael@0 331 for (var member in set) {
michael@0 332 if (set.hasOwnProperty(member)) {
michael@0 333 processForEachMember(state, member, templNode, sibling,
michael@0 334 data, paramName, member);
michael@0 335 }
michael@0 336 }
michael@0 337 }
michael@0 338 }
michael@0 339
michael@0 340 /**
michael@0 341 * Called by processForEachLoop() to resolve any promises in the array (the
michael@0 342 * array itself can also be a promise, but that is resolved by
michael@0 343 * processForEach()). Handle <LOOP> elements (which are taken out of the DOM),
michael@0 344 * clone the template node, and pass the processing on to processNode().
michael@0 345 * @param member The data item to use in templating
michael@0 346 * @param templNode The node to copy for each set member
michael@0 347 * @param siblingNode The parent node to which we add things
michael@0 348 * @param data the data to use for node processing
michael@0 349 * @param paramName The name given to 'member' by the foreach attribute
michael@0 350 * @param frame A name to push on the stack for debugging
michael@0 351 */
michael@0 352 function processForEachMember(state, member, templNode, siblingNode, data, paramName, frame) {
michael@0 353 state.stack.push(frame);
michael@0 354 try {
michael@0 355 var cState = cloneState(state);
michael@0 356 handleAsync(member, siblingNode, function(reply, node) {
michael@0 357 data[paramName] = reply;
michael@0 358 if (node.parentNode != null) {
michael@0 359 if (templNode.nodeName.toLowerCase() === 'loop') {
michael@0 360 for (var i = 0; i < templNode.childNodes.length; i++) {
michael@0 361 var clone = templNode.childNodes[i].cloneNode(true);
michael@0 362 node.parentNode.insertBefore(clone, node);
michael@0 363 processNode(cState, clone, data);
michael@0 364 }
michael@0 365 }
michael@0 366 else {
michael@0 367 var clone = templNode.cloneNode(true);
michael@0 368 clone.removeAttribute('foreach');
michael@0 369 node.parentNode.insertBefore(clone, node);
michael@0 370 processNode(cState, clone, data);
michael@0 371 }
michael@0 372 }
michael@0 373 delete data[paramName];
michael@0 374 });
michael@0 375 }
michael@0 376 finally {
michael@0 377 state.stack.pop();
michael@0 378 }
michael@0 379 }
michael@0 380
michael@0 381 /**
michael@0 382 * Take a text node and replace it with another text node with the ${...}
michael@0 383 * sections parsed out. We replace the node by altering node.parentNode but
michael@0 384 * we could probably use a DOM Text API to achieve the same thing.
michael@0 385 * @param node The Text node to work on
michael@0 386 * @param data The data to use in calls to envEval()
michael@0 387 */
michael@0 388 function processTextNode(state, node, data) {
michael@0 389 // Replace references in other attributes
michael@0 390 var value = node.data;
michael@0 391 // We can't use the string.replace() with function trick (see generic
michael@0 392 // attribute processing in processNode()) because we need to support
michael@0 393 // functions that return DOM nodes, so we can't have the conversion to a
michael@0 394 // string.
michael@0 395 // Instead we process the string as an array of parts. In order to split
michael@0 396 // the string up, we first replace '${' with '\uF001$' and '}' with '\uF002'
michael@0 397 // We can then split using \uF001 or \uF002 to get an array of strings
michael@0 398 // where scripts are prefixed with $.
michael@0 399 // \uF001 and \uF002 are just unicode chars reserved for private use.
michael@0 400 value = value.replace(TEMPLATE_REGION, '\uF001$$$1\uF002');
michael@0 401 // Split a string using the unicode chars F001 and F002.
michael@0 402 var parts = value.split(/\uF001|\uF002/);
michael@0 403 if (parts.length > 1) {
michael@0 404 parts.forEach(function(part) {
michael@0 405 if (part === null || part === undefined || part === '') {
michael@0 406 return;
michael@0 407 }
michael@0 408 if (part.charAt(0) === '$') {
michael@0 409 part = envEval(state, part.slice(1), data, node.data);
michael@0 410 }
michael@0 411 var cState = cloneState(state);
michael@0 412 handleAsync(part, node, function(reply, siblingNode) {
michael@0 413 var doc = siblingNode.ownerDocument;
michael@0 414 if (reply == null) {
michael@0 415 reply = cState.options.blankNullUndefined ? '' : '' + reply;
michael@0 416 }
michael@0 417 if (typeof reply.cloneNode === 'function') {
michael@0 418 // i.e. if (reply instanceof Element) { ...
michael@0 419 reply = maybeImportNode(cState, reply, doc);
michael@0 420 siblingNode.parentNode.insertBefore(reply, siblingNode);
michael@0 421 }
michael@0 422 else if (typeof reply.item === 'function' && reply.length) {
michael@0 423 // NodeLists can be live, in which case maybeImportNode can
michael@0 424 // remove them from the document, and thus the NodeList, which in
michael@0 425 // turn breaks iteration. So first we clone the list
michael@0 426 var list = Array.prototype.slice.call(reply, 0);
michael@0 427 list.forEach(function(child) {
michael@0 428 var imported = maybeImportNode(cState, child, doc);
michael@0 429 siblingNode.parentNode.insertBefore(imported, siblingNode);
michael@0 430 });
michael@0 431 }
michael@0 432 else {
michael@0 433 // if thing isn't a DOM element then wrap its string value in one
michael@0 434 reply = doc.createTextNode(reply.toString());
michael@0 435 siblingNode.parentNode.insertBefore(reply, siblingNode);
michael@0 436 }
michael@0 437 });
michael@0 438 });
michael@0 439 node.parentNode.removeChild(node);
michael@0 440 }
michael@0 441 }
michael@0 442
michael@0 443 /**
michael@0 444 * Return node or a import of node, if it's not in the given document
michael@0 445 * @param node The node that we want to be properly owned
michael@0 446 * @param doc The document that the given node should belong to
michael@0 447 * @return A node that belongs to the given document
michael@0 448 */
michael@0 449 function maybeImportNode(state, node, doc) {
michael@0 450 return node.ownerDocument === doc ? node : doc.importNode(node, true);
michael@0 451 }
michael@0 452
michael@0 453 /**
michael@0 454 * A function to handle the fact that some nodes can be promises, so we check
michael@0 455 * and resolve if needed using a marker node to keep our place before calling
michael@0 456 * an inserter function.
michael@0 457 * @param thing The object which could be real data or a promise of real data
michael@0 458 * we use it directly if it's not a promise, or resolve it if it is.
michael@0 459 * @param siblingNode The element before which we insert new elements.
michael@0 460 * @param inserter The function to to the insertion. If thing is not a promise
michael@0 461 * then handleAsync() is just 'inserter(thing, siblingNode)'
michael@0 462 */
michael@0 463 function handleAsync(thing, siblingNode, inserter) {
michael@0 464 if (thing != null && typeof thing.then === 'function') {
michael@0 465 // Placeholder element to be replaced once we have the real data
michael@0 466 var tempNode = siblingNode.ownerDocument.createElement('span');
michael@0 467 siblingNode.parentNode.insertBefore(tempNode, siblingNode);
michael@0 468 thing.then(function(delayed) {
michael@0 469 inserter(delayed, tempNode);
michael@0 470 if (tempNode.parentNode != null) {
michael@0 471 tempNode.parentNode.removeChild(tempNode);
michael@0 472 }
michael@0 473 }).then(null, function(error) {
michael@0 474 console.error(error.stack);
michael@0 475 });
michael@0 476 }
michael@0 477 else {
michael@0 478 inserter(thing, siblingNode);
michael@0 479 }
michael@0 480 }
michael@0 481
michael@0 482 /**
michael@0 483 * Warn of string does not begin '${' and end '}'
michael@0 484 * @param str the string to check.
michael@0 485 * @return The string stripped of ${ and }, or untouched if it does not match
michael@0 486 */
michael@0 487 function stripBraces(state, str) {
michael@0 488 if (!str.match(TEMPLATE_REGION)) {
michael@0 489 handleError(state, 'Expected ' + str + ' to match ${...}');
michael@0 490 return str;
michael@0 491 }
michael@0 492 return str.slice(2, -1);
michael@0 493 }
michael@0 494
michael@0 495 /**
michael@0 496 * Combined getter and setter that works with a path through some data set.
michael@0 497 * For example:
michael@0 498 * <ul>
michael@0 499 * <li>property(state, 'a.b', { a: { b: 99 }}); // returns 99
michael@0 500 * <li>property(state, 'a', { a: { b: 99 }}); // returns { b: 99 }
michael@0 501 * <li>property(state, 'a', { a: { b: 99 }}, 42); // returns 99 and alters the
michael@0 502 * input data to be { a: { b: 42 }}
michael@0 503 * </ul>
michael@0 504 * @param path An array of strings indicating the path through the data, or
michael@0 505 * a string to be cut into an array using <tt>split('.')</tt>
michael@0 506 * @param data the data to use for node processing
michael@0 507 * @param newValue (optional) If defined, this value will replace the
michael@0 508 * original value for the data at the path specified.
michael@0 509 * @return The value pointed to by <tt>path</tt> before any
michael@0 510 * <tt>newValue</tt> is applied.
michael@0 511 */
michael@0 512 function property(state, path, data, newValue) {
michael@0 513 try {
michael@0 514 if (typeof path === 'string') {
michael@0 515 path = path.split('.');
michael@0 516 }
michael@0 517 var value = data[path[0]];
michael@0 518 if (path.length === 1) {
michael@0 519 if (newValue !== undefined) {
michael@0 520 data[path[0]] = newValue;
michael@0 521 }
michael@0 522 if (typeof value === 'function') {
michael@0 523 return value.bind(data);
michael@0 524 }
michael@0 525 return value;
michael@0 526 }
michael@0 527 if (!value) {
michael@0 528 handleError(state, '"' + path[0] + '" is undefined');
michael@0 529 return null;
michael@0 530 }
michael@0 531 return property(state, path.slice(1), value, newValue);
michael@0 532 }
michael@0 533 catch (ex) {
michael@0 534 handleError(state, 'Path error with \'' + path + '\'', ex);
michael@0 535 return '${' + path + '}';
michael@0 536 }
michael@0 537 }
michael@0 538
michael@0 539 /**
michael@0 540 * Like eval, but that creates a context of the variables in <tt>env</tt> in
michael@0 541 * which the script is evaluated.
michael@0 542 * WARNING: This script uses 'with' which is generally regarded to be evil.
michael@0 543 * The alternative is to create a Function at runtime that takes X parameters
michael@0 544 * according to the X keys in the env object, and then call that function using
michael@0 545 * the values in the env object. This is likely to be slow, but workable.
michael@0 546 * @param script The string to be evaluated.
michael@0 547 * @param data The environment in which to eval the script.
michael@0 548 * @param frame Optional debugging string in case of failure.
michael@0 549 * @return The return value of the script, or the error message if the script
michael@0 550 * execution failed.
michael@0 551 */
michael@0 552 function envEval(state, script, data, frame) {
michael@0 553 try {
michael@0 554 state.stack.push(frame.replace(/\s+/g, ' '));
michael@0 555 // Detect if a script is capable of being interpreted using property()
michael@0 556 if (/^[_a-zA-Z0-9.]*$/.test(script)) {
michael@0 557 return property(state, script, data);
michael@0 558 }
michael@0 559 else {
michael@0 560 if (!state.options.allowEval) {
michael@0 561 handleError(state, 'allowEval is not set, however \'' + script + '\'' +
michael@0 562 ' can not be resolved using a simple property path.');
michael@0 563 return '${' + script + '}';
michael@0 564 }
michael@0 565 with (data) {
michael@0 566 return eval(script);
michael@0 567 }
michael@0 568 }
michael@0 569 }
michael@0 570 catch (ex) {
michael@0 571 handleError(state, 'Template error evaluating \'' + script + '\'', ex);
michael@0 572 return '${' + script + '}';
michael@0 573 }
michael@0 574 finally {
michael@0 575 state.stack.pop();
michael@0 576 }
michael@0 577 }
michael@0 578
michael@0 579 /**
michael@0 580 * A generic way of reporting errors, for easy overloading in different
michael@0 581 * environments.
michael@0 582 * @param message the error message to report.
michael@0 583 * @param ex optional associated exception.
michael@0 584 */
michael@0 585 function handleError(state, message, ex) {
michael@0 586 logError(message + ' (In: ' + state.stack.join(' > ') + ')');
michael@0 587 if (ex) {
michael@0 588 logError(ex);
michael@0 589 }
michael@0 590 }
michael@0 591
michael@0 592 /**
michael@0 593 * A generic way of reporting errors, for easy overloading in different
michael@0 594 * environments.
michael@0 595 * @param message the error message to report.
michael@0 596 */
michael@0 597 function logError(message) {
michael@0 598 console.log(message);
michael@0 599 }

mercurial