Wed, 31 Dec 2014 06:09:35 +0100
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 | } |