1/** 2 * Twig.js 0.8.3 3 * 4 * @copyright 2011-2015 John Roepke and the Twig.js Contributors 5 * @license Available under the BSD 2-Clause License 6 * @link https://github.com/justjohn/twig.js 7 */ 8 9var Twig = (function (Twig) { 10 11 Twig.VERSION = "0.8.4"; 12 13 return Twig; 14})(Twig || {}); 15// Twig.js 16// Available under the BSD 2-Clause License 17// https://github.com/justjohn/twig.js 18 19var Twig = (function (Twig) { 20 "use strict"; 21 // ## twig.core.js 22 // 23 // This file handles template level tokenizing, compiling and parsing. 24 25 Twig.trace = false; 26 Twig.debug = false; 27 28 // Default caching to true for the improved performance it offers 29 Twig.cache = true; 30 31 Twig.placeholders = { 32 parent: "{{|PARENT|}}" 33 }; 34 35 /** 36 * Fallback for Array.indexOf for IE8 et al 37 */ 38 Twig.indexOf = function (arr, searchElement /*, fromIndex */ ) { 39 if (Array.prototype.hasOwnProperty("indexOf")) { 40 return arr.indexOf(searchElement); 41 } 42 if (arr === void 0 || arr === null) { 43 throw new TypeError(); 44 } 45 var t = Object(arr); 46 var len = t.length >>> 0; 47 if (len === 0) { 48 return -1; 49 } 50 var n = 0; 51 if (arguments.length > 0) { 52 n = Number(arguments[1]); 53 if (n !== n) { // shortcut for verifying if it's NaN 54 n = 0; 55 } else if (n !== 0 && n !== Infinity && n !== -Infinity) { 56 n = (n > 0 || -1) * Math.floor(Math.abs(n)); 57 } 58 } 59 if (n >= len) { 60 // console.log("indexOf not found1 ", JSON.stringify(searchElement), JSON.stringify(arr)); 61 return -1; 62 } 63 var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); 64 for (; k < len; k++) { 65 if (k in t && t[k] === searchElement) { 66 return k; 67 } 68 } 69 if (arr == searchElement) { 70 return 0; 71 } 72 // console.log("indexOf not found2 ", JSON.stringify(searchElement), JSON.stringify(arr)); 73 74 return -1; 75 } 76 77 Twig.forEach = function (arr, callback, thisArg) { 78 if (Array.prototype.forEach ) { 79 return arr.forEach(callback, thisArg); 80 } 81 82 var T, k; 83 84 if ( arr == null ) { 85 throw new TypeError( " this is null or not defined" ); 86 } 87 88 // 1. Let O be the result of calling ToObject passing the |this| value as the argument. 89 var O = Object(arr); 90 91 // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length". 92 // 3. Let len be ToUint32(lenValue). 93 var len = O.length >>> 0; // Hack to convert O.length to a UInt32 94 95 // 4. If IsCallable(callback) is false, throw a TypeError exception. 96 // See: http://es5.github.com/#x9.11 97 if ( {}.toString.call(callback) != "[object Function]" ) { 98 throw new TypeError( callback + " is not a function" ); 99 } 100 101 // 5. If thisArg was supplied, let T be thisArg; else let T be undefined. 102 if ( thisArg ) { 103 T = thisArg; 104 } 105 106 // 6. Let k be 0 107 k = 0; 108 109 // 7. Repeat, while k < len 110 while( k < len ) { 111 112 var kValue; 113 114 // a. Let Pk be ToString(k). 115 // This is implicit for LHS operands of the in operator 116 // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk. 117 // This step can be combined with c 118 // c. If kPresent is true, then 119 if ( k in O ) { 120 121 // i. Let kValue be the result of calling the Get internal method of O with argument Pk. 122 kValue = O[ k ]; 123 124 // ii. Call the Call internal method of callback with T as the this value and 125 // argument list containing kValue, k, and O. 126 callback.call( T, kValue, k, O ); 127 } 128 // d. Increase k by 1. 129 k++; 130 } 131 // 8. return undefined 132 }; 133 134 Twig.merge = function(target, source, onlyChanged) { 135 Twig.forEach(Object.keys(source), function (key) { 136 if (onlyChanged && !(key in target)) { 137 return; 138 } 139 140 target[key] = source[key] 141 }); 142 143 return target; 144 }; 145 146 /** 147 * Exception thrown by twig.js. 148 */ 149 Twig.Error = function(message) { 150 this.message = message; 151 this.name = "TwigException"; 152 this.type = "TwigException"; 153 }; 154 155 /** 156 * Get the string representation of a Twig error. 157 */ 158 Twig.Error.prototype.toString = function() { 159 var output = this.name + ": " + this.message; 160 161 return output; 162 }; 163 164 /** 165 * Wrapper for logging to the console. 166 */ 167 Twig.log = { 168 trace: function() {if (Twig.trace && console) {console.log(Array.prototype.slice.call(arguments));}}, 169 debug: function() {if (Twig.debug && console) {console.log(Array.prototype.slice.call(arguments));}} 170 }; 171 172 if (typeof console !== "undefined") { 173 if (typeof console.error !== "undefined") { 174 Twig.log.error = function() { 175 console.error.apply(console, arguments); 176 } 177 } else if (typeof console.log !== "undefined") { 178 Twig.log.error = function() { 179 console.log.apply(console, arguments); 180 } 181 } 182 } else { 183 Twig.log.error = function(){}; 184 } 185 186 /** 187 * Wrapper for child context objects in Twig. 188 * 189 * @param {Object} context Values to initialize the context with. 190 */ 191 Twig.ChildContext = function(context) { 192 var ChildContext = function ChildContext() {}; 193 ChildContext.prototype = context; 194 return new ChildContext(); 195 }; 196 197 /** 198 * Container for methods related to handling high level template tokens 199 * (for example: {{ expression }}, {% logic %}, {# comment #}, raw data) 200 */ 201 Twig.token = {}; 202 203 /** 204 * Token types. 205 */ 206 Twig.token.type = { 207 output: 'output', 208 logic: 'logic', 209 comment: 'comment', 210 raw: 'raw' 211 }; 212 213 /** 214 * Token syntax definitions. 215 */ 216 Twig.token.definitions = [ 217 { 218 type: Twig.token.type.raw, 219 open: '{% raw %}', 220 close: '{% endraw %}' 221 }, 222 { 223 type: Twig.token.type.raw, 224 open: '{% verbatim %}', 225 close: '{% endverbatim %}' 226 }, 227 // *Output type tokens* 228 // 229 // These typically take the form `{{ expression }}`. 230 { 231 type: Twig.token.type.output, 232 open: '{{', 233 close: '}}' 234 }, 235 // *Logic type tokens* 236 // 237 // These typically take a form like `{% if expression %}` or `{% endif %}` 238 { 239 type: Twig.token.type.logic, 240 open: '{%', 241 close: '%}' 242 }, 243 // *Comment type tokens* 244 // 245 // These take the form `{# anything #}` 246 { 247 type: Twig.token.type.comment, 248 open: '{#', 249 close: '#}' 250 } 251 ]; 252 253 254 /** 255 * What characters start "strings" in token definitions. We need this to ignore token close 256 * strings inside an expression. 257 */ 258 Twig.token.strings = ['"', "'"]; 259 260 Twig.token.findStart = function (template) { 261 var output = { 262 position: null, 263 def: null 264 }, 265 i, 266 token_template, 267 first_key_position; 268 269 for (i=0;i<Twig.token.definitions.length;i++) { 270 token_template = Twig.token.definitions[i]; 271 first_key_position = template.indexOf(token_template.open); 272 273 Twig.log.trace("Twig.token.findStart: ", "Searching for ", token_template.open, " found at ", first_key_position); 274 275 // Does this token occur before any other types? 276 if (first_key_position >= 0 && (output.position === null || first_key_position < output.position)) { 277 output.position = first_key_position; 278 output.def = token_template; 279 } 280 } 281 282 return output; 283 }; 284 285 Twig.token.findEnd = function (template, token_def, start) { 286 var end = null, 287 found = false, 288 offset = 0, 289 290 // String position variables 291 str_pos = null, 292 str_found = null, 293 pos = null, 294 end_offset = null, 295 this_str_pos = null, 296 end_str_pos = null, 297 298 // For loop variables 299 i, 300 l; 301 302 while (!found) { 303 str_pos = null; 304 str_found = null; 305 pos = template.indexOf(token_def.close, offset); 306 307 if (pos >= 0) { 308 end = pos; 309 found = true; 310 } else { 311 // throw an exception
312 throw new Twig.Error("Unable to find closing bracket '" + token_def.close + 313 "'" + " opened near template position " + start); 314 } 315 316 // Ignore quotes within comments; just look for the next comment close sequence, 317 // regardless of what comes before it. https://github.com/justjohn/twig.js/issues/95 318 if (token_def.type === Twig.token.type.comment) { 319 break; 320 } 321 322 l = Twig.token.strings.length; 323 for (i = 0; i < l; i += 1) { 324 this_str_pos = template.indexOf(Twig.token.strings[i], offset); 325 326 if (this_str_pos > 0 && this_str_pos < pos && 327 (str_pos === null || this_str_pos < str_pos)) { 328 str_pos = this_str_pos; 329 str_found = Twig.token.strings[i]; 330 } 331 } 332 333 // We found a string before the end of the token, now find the string's end and set the search offset to it 334 if (str_pos !== null) { 335 end_offset = str_pos + 1; 336 end = null; 337 found = false; 338 while (true) { 339 end_str_pos = template.indexOf(str_found, end_offset); 340 if (end_str_pos < 0) { 341 throw "Unclosed string in template"; 342 } 343 // Ignore escaped quotes 344 if (template.substr(end_str_pos - 1, 1) !== "\\") { 345 offset = end_str_pos + 1; 346 break; 347 } else { 348 end_offset = end_str_pos + 1; 349 } 350 } 351 } 352 } 353 return end; 354 }; 355 356 /** 357 * Convert a template into high-level tokens. 358 */ 359 Twig.tokenize = function (template) { 360 var tokens = [], 361 // An offset for reporting errors locations in the template. 362 error_offset = 0, 363 364 // The start and type of the first token found in the template. 365 found_token = null, 366 // The end position of the matched token. 367 end = null; 368 369 while (template.length > 0) { 370 // Find the first occurance of any token type in the template 371 found_token = Twig.token.findStart(template); 372 373 Twig.log.trace("Twig.tokenize: ", "Found token: ", found_token); 374 375 if (found_token.position !== null) { 376 // Add a raw type token for anything before the start of the token 377 if (found_token.position > 0) { 378 tokens.push({ 379 type: Twig.token.type.raw, 380 value: template.substring(0, found_token.position) 381 }); 382 } 383 template = template.substr(found_token.position + found_token.def.open.length); 384 error_offset += found_token.position + found_token.def.open.length; 385 386 // Find the end of the token 387 end = Twig.token.findEnd(template, found_token.def, error_offset); 388 389 Twig.log.trace("Twig.tokenize: ", "Token ends at ", end); 390 391 tokens.push({ 392 type: found_token.def.type, 393 value: template.substring(0, end).trim() 394 }); 395 396 if ( found_token.def.type === "logic" && template.substr( end + found_token.def.close.length, 1 ) === "\n" ) { 397 // Newlines directly after logic tokens are ignored 398 end += 1; 399 } 400 401 template = template.substr(end + found_token.def.close.length); 402 403 // Increment the position in the template 404 error_offset += end + found_token.def.close.length; 405 406 } else { 407 // No more tokens -> add the rest of the template as a raw-type token 408 tokens.push({ 409 type: Twig.token.type.raw, 410 value: template 411 }); 412 template = ''; 413 } 414 } 415 416 return tokens; 417 }; 418 419 420 Twig.compile = function (tokens) { 421 try { 422 423 // Output and intermediate stacks 424 var output = [], 425 stack = [], 426 // The tokens between open and close tags 427 intermediate_output = [], 428 429 token = null, 430 logic_token = null, 431 unclosed_token = null, 432 // Temporary previous token. 433 prev_token = null, 434 // The previous token's template 435 prev_template = null, 436 // The output token 437 tok_output = null, 438 439 // Logic Token values 440 type = null, 441 open = null, 442 next = null; 443 444 while (tokens.length > 0) { 445 token = tokens.shift(); 446 Twig.log.trace("Compiling token ", token); 447 switch (token.type) { 448 case Twig.token.type.raw: 449 if (stack.length > 0) { 450 intermediate_output.push(token); 451 } else { 452 output.push(token); 453 } 454 break; 455 456 case Twig.token.type.logic: 457 // Compile the logic token 458 logic_token = Twig.logic.compile.apply(this, [token]); 459 460 type = logic_token.type; 461 open = Twig.logic.handler[type].open; 462 next = Twig.logic.handler[type].next; 463 464 Twig.log.trace("Twig.compile: ", "Compiled logic token to ", logic_token, 465 " next is: ", next, " open is : ", open); 466 467 // Not a standalone token, check logic stack to see if this is expected 468 if (open !== undefined && !open) { 469 prev_token = stack.pop(); 470 prev_template = Twig.logic.handler[prev_token.type]; 471 472 if (Twig.indexOf(prev_template.next, type) < 0) { 473 throw new Error(type + " not expected after a " + prev_token.type); 474 } 475 476 prev_token.output = prev_token.output || []; 477 478 prev_token.output = prev_token.output.concat(intermediate_output); 479 intermediate_output = []; 480 481 tok_output = { 482 type: Twig.token.type.logic, 483 token: prev_token 484 }; 485 if (stack.length > 0) { 486 intermediate_output.push(tok_output); 487 } else { 488 output.push(tok_output); 489 } 490 } 491 492 // This token requires additional tokens to complete the logic structure. 493 if (next !== undefined && next.length > 0) { 494 Twig.log.trace("Twig.compile: ", "Pushing ", logic_token, " to logic stack."); 495 496 if (stack.length > 0) { 497 // Put any currently held output into the output list of the logic operator
498 // currently at the head of the stack before we push a new one on. 499 prev_token = stack.pop(); 500 prev_token.output = prev_token.output || []; 501 prev_token.output = prev_token.output.concat(intermediate_output); 502 stack.push(prev_token); 503 intermediate_output = []; 504 } 505 506 // Push the new logic token onto the logic stack 507 stack.push(logic_token); 508 509 } else if (open !== undefined && open) { 510 tok_output = { 511 type: Twig.token.type.logic, 512 token: logic_token 513 }; 514 // Standalone token (like {% set ... %} 515 if (stack.length > 0) { 516 intermediate_output.push(tok_output); 517 } else { 518 output.push(tok_output); 519 } 520 } 521 break; 522 523 // Do nothing, comments should be ignored 524 case Twig.token.type.comment: 525 break; 526 527 case Twig.token.type.output: 528 Twig.expression.compile.apply(this, [token]); 529 if (stack.length > 0) { 530 intermediate_output.push(token); 531 } else { 532 output.push(token); 533 } 534 break; 535 } 536 537 Twig.log.trace("Twig.compile: ", " Output: ", output, 538 " Logic Stack: ", stack, 539 " Pending Output: ", intermediate_output ); 540 } 541 542 // Verify that there are no logic tokens left in the stack. 543 if (stack.length > 0) { 544 unclosed_token = stack.pop(); 545 throw new Error("Unable to find an end tag for " + unclosed_token.type + 546 ", expecting one of " + unclosed_token.next); 547 } 548 return output; 549 } catch (ex) { 550 Twig.log.error("Error compiling twig template " + this.id + ": "); 551 if (ex.stack) { 552 Twig.log.error(ex.stack); 553 } else { 554 Twig.log.error(ex.toString()); 555 } 556 557 if (this.options.rethrow) throw ex; 558 } 559 }; 560 561 /** 562 * Parse a compiled template. 563 * 564 * @param {Array} tokens The compiled tokens. 565 * @param {Object} context The render context. 566 * 567 * @return {string} The parsed template. 568 */ 569 Twig.parse = function (tokens, context) { 570 try { 571 var output = [], 572 // Track logic chains 573 chain = true, 574 that = this; 575 576 Twig.forEach(tokens, function parseToken(token) { 577 Twig.log.debug("Twig.parse: ", "Parsing token: ", token); 578 579 switch (token.type) { 580 case Twig.token.type.raw: 581 output.push(Twig.filters.raw(token.value)); 582 break; 583 584 case Twig.token.type.logic: 585 var logic_token = token.token, 586 logic = Twig.logic.parse.apply(that, [logic_token, context, chain]); 587 588 if (logic.chain !== undefined) { 589 chain = logic.chain; 590 } 591 if (logic.context !== undefined) { 592 context = logic.context; 593 } 594 if (logic.output !== undefined) { 595 output.push(logic.output); 596 } 597 break; 598 599 case Twig.token.type.comment:
600 // Do nothing, comments should be ignored 601 break; 602 603 case Twig.token.type.output: 604 Twig.log.debug("Twig.parse: ", "Output token: ", token.stack); 605 // Parse the given expression in the given context 606 output.push(Twig.expression.parse.apply(that, [token.stack, context])); 607 break; 608 } 609 }); 610 return Twig.output.apply(this, [output]); 611 } catch (ex) { 612 Twig.log.error("Error parsing twig template " + this.id + ": "); 613 if (ex.stack) { 614 Twig.log.error(ex.stack); 615 } else { 616 Twig.log.error(ex.toString()); 617 } 618 619 if (this.options.rethrow) throw ex; 620 621 if (Twig.debug) { 622 return ex.toString(); 623 } 624 } 625 }; 626 627 /** 628 * Tokenize and compile a string template. 629 * 630 * @param {string} data The template. 631 * 632 * @return {Array} The compiled tokens. 633 */ 634 Twig.prepare = function(data) { 635 var tokens, raw_tokens; 636 637 // Tokenize 638 Twig.log.debug("Twig.prepare: ", "Tokenizing ", data); 639 raw_tokens = Twig.tokenize.apply(this, [data]); 640 641 // Compile 642 Twig.log.debug("Twig.prepare: ", "Compiling ", raw_tokens); 643 tokens = Twig.compile.apply(this, [raw_tokens]); 644 645 Twig.log.debug("Twig.prepare: ", "Compiled ", tokens); 646 647 return tokens; 648 }; 649 650 /** 651 * Join the output token's stack and escape it if needed 652 * 653 * @param {Array} Output token's stack 654 * 655 * @return {string|String} Autoescaped output 656 */ 657 Twig.output = function(output) { 658 if (!this.options.autoescape) { 659 return output.join(""); 660 } 661 662 // [].map would be better but it's not supported by IE8- 663 var escaped_output = []; 664 Twig.forEach(output, function (str) { 665 if (str && !str.twig_markup) { 666 str = Twig.filters.escape(str); 667 } 668 escaped_output.push(str); 669 }); 670 return Twig.Markup(escaped_output.join("")); 671 } 672 673 // Namespace for template storage and retrieval 674 Twig.Templates = { 675 registry: {} 676 }; 677 678 /** 679 * Is this id valid for a twig template? 680 * 681 * @param {string} id The ID to check. 682 * 683 * @throws {Twig.Error}
683 If the ID is invalid or used. 684 * @return {boolean} True if the ID is valid. 685 */ 686 Twig.validateId = function(id) { 687 if (id === "prototype") { 688 throw new Twig.Error(id + " is not a valid twig identifier"); 689 } else if (Twig.Templates.registry.hasOwnProperty(id)) { 690 throw new Twig.Error("There is already a template with the ID " + id); 691 } 692 return true; 693 } 694 695 /** 696 * Save a template object to the store. 697 * 698 * @param {Twig.Template} template The twig.js template to store. 699 */ 700 Twig.Templates.save = function(template) { 701 if (template.id === undefined) { 702 throw new Twig.Error("Unable to save template with no id"); 703 } 704 Twig.Templates.registry[template.id] = template; 705 }; 706 707 /** 708 * Load a previously saved template from the store. 709 * 710 * @param {string} id The ID of the template to load. 711 * 712 * @return {Twig.Template} A twig.js template stored with the provided ID. 713 */ 714 Twig.Templates.load = function(id) { 715 if (!Twig.Templates.registry.hasOwnProperty(id)) { 716 return null; 717 } 718 return Twig.Templates.registry[id]; 719 }; 720 721 /** 722 * Load a template from a remote location using AJAX and saves in with the given ID. 723 * 724 * Available parameters: 725 * 726 * async: Should the HTTP request be performed asynchronously. 727 * Defaults to true. 728 * method: What method should be used to load the template 729 * (fs or ajax) 730 * precompiled: Has the template already been compiled. 731 * 732 * @param {string} location The remote URL to load as a template. 733 * @param {Object} params The template parameters. 734 * @param {function} callback A callback triggered when the template finishes loading. 735 * @param {function} error_callback A callback triggered if an error occurs loading the template. 736 * 737 * 738 */ 739 Twig.Templates.loadRemote = function(location, params, callback, error_callback) { 740 var id = params.id, 741 method = params.method, 742 async = params.async, 743 precompiled = params.precompiled, 744 template = null; 745 746 // Default to async 747 if (async === undefined) async = true; 748 749 // Default to the URL so the template is cached. 750 if (id === undefined) { 751 id = location; 752 } 753 params.id = id; 754 755 // Check for existing template 756 if (Twig.cache && Twig.Templates.registry.hasOwnProperty(id)) { 757 // A template is already saved with the given id. 758 if (callback) { 759 callback(Twig.Templates.registry[id]); 760 } 761 return Twig.Templates.registry[id]; 762 } 763 764 if (method == 'ajax') { 765 if (typeof XMLHttpRequest == "undefined") { 766 throw new Twig.Error("Unsupported platform: Unable to do remote requests " + 767 "because there is no XMLHTTPRequest implementation"); 768 } 769 770 var xmlhttp = new XMLHttpRequest(); 771 xmlhttp.onreadystatechange = function() { 772 var data = null; 773 774 if(xmlhttp.readyState == 4) { 775 if (xmlhttp.status == 200) { 776 Twig.log.debug("Got template ", xmlhttp.responseText); 777 778 if (precompiled === true) { 779 data = JSON.parse(xmlhttp.responseText); 780 } else { 781 data = xmlhttp.responseText; 782 } 783 784 params.url = location; 785 params.data = data; 786 787 template = new Twig.Template(params); 788 789 if (callback) { 790 callback(template); 791 } 792 } else { 793 if (error_callback) { 794 error_callback(xmlhttp); 795 } 796 } 797 } 798 }; 799 xmlhttp.open("GET", location, async); 800 xmlhttp.send(); 801 802 } else { // if method = 'fs' 803 // Create local scope 804 (function() { 805 var fs = require('fs'), 806 path = require('path'), 807 data = null, 808 loadTemplateFn = function(err, data) { 809 if (err) { 810 if (error_callback) { 811 error_callback(err); 812 } 813 return; 814 } 815 816 if (precompiled === true) { 817 data = JSON.parse(data); 818 } 819 820 params.data = data; 821 params.path = location; 822 823 // template is in data 824 template = new Twig.Template(params); 825 826 if (callback) { 827 callback(template); 828 } 829 }; 830 831 if (async === true) { 832 fs.stat(location, function (err, stats) { 833 if (err || !stats.isFile())
834 throw new Twig.Error("Unable to find template file " + location); 835 836 fs.readFile(location, 'utf8', loadTemplateFn); 837 }); 838 } else { 839 if (!fs.statSync(location).isFile()) 840 throw new Twig.Error("Unable to find template file " + location); 841 842 data = fs.readFileSync(location, 'utf8'); 843 loadTemplateFn(undefined, data); 844 } 845 })(); 846 } 847 if (async === false) { 848 return template; 849 } else { 850 // placeholder for now, should eventually return a deferred object. 851 return true; 852 } 853 }; 854 855 // Determine object type 856 function is(type, obj) { 857 var clas = Object.prototype.toString.call(obj).slice(8, -1); 858 return obj !== undefined && obj !== null && clas === type; 859 } 860 861 /** 862 * Create a new twig.js template. 863 * 864 * Parameters: { 865 * data: The template, either pre-compiled tokens or a string template 866 * id: The name of this template 867 * blocks: Any pre-existing block from a child template 868 * } 869 * 870 * @param {Object} params The template parameters. 871 */ 872 Twig.Template = function ( params ) { 873 var data = params.data, 874 id = params.id, 875 blocks = params.blocks, 876 macros = params.macros || {}, 877 base = params.base, 878 path = params.path, 879 url = params.url, 880 // parser options 881 options = params.options; 882 883 // # What is stored in a Twig.Template 884 // 885 // The Twig Template hold several chucks of data. 886 // 887 // { 888 // id: The token ID (if any) 889 // tokens: The list of tokens that makes up this template. 890 // blocks: The list of block this template contains. 891 // base: The base template (if any) 892 // options: { 893 // Compiler/parser options 894 // 895 // strict_variables: true/false 896 // Should missing variable/keys emit an error message. If false, they default to null. 897 // } 898 // } 899 // 900 901 this.id = id; 902 this.base = base; 903 this.path = path; 904 this.url = url; 905 this.macros = macros; 906 this.options = options; 907 908 this.reset(blocks); 909 910 if (is('String', data)) { 911 this.tokens = Twig.prepare.apply(this, [data]); 912 } else { 913 this.tokens = data; 914 } 915 916 if (id !== undefined) { 917 Twig.Templates.save(this); 918 } 919 }; 920 921 Twig.Template.prototype.reset = function(blocks) { 922 Twig.log.debug("Twig.Template.reset", "Reseting template " + this.id); 923 this.blocks = {}; 924 this.importedBlocks = []; 925 this.originalBlockTokens = {}; 926 this.child = { 927 blocks: blocks || {} 928 }; 929 this.extend = null; 930 }; 931 932 Twig.Template.prototype.render = function (context, params) { 933 params = params || {}; 934 935 var output, 936 url; 937 938 this.context = context || {}; 939 940 // Clear any previous state 941 this.reset(); 942 if (params.blocks) { 943 this.blocks = params.blocks; 944 } 945 if (params.macros) { 946 this.macros = params.macros; 947 } 948 949 output = Twig.parse.apply(this, [this.tokens, this.context]); 950 951 // Does this template extend another 952 if (this.extend) { 953 var ext_template; 954 955 // check if the template is provided inline 956 if ( this.options.allowInlineIncludes ) { 957 ext_template = Twig.Templates.load(this.extend); 958 if ( ext_template ) { 959 ext_template.options = this.options; 960 } 961 } 962 963 // check for the template file via include 964 if (!ext_template) { 965 url = relativePath(this, this.extend); 966 967 ext_template = Twig.Templates.loadRemote(url, { 968 method: this.url?'ajax':'fs', 969 base: this.base, 970 async: false, 971 id: url, 972 options: this.options 973 }); 974 } 975 976 this.parent = ext_template; 977 978 return this.parent.render(this.context, { 979 blocks: this.blocks 980 }); 981 } 982 983 if (params.output == 'blocks') { 984 return this.blocks; 985 } else if (params.output == 'macros') { 986 return this.macros; 987 } else { 988 return output; 989 } 990 }; 991 992 Twig.Template.prototype.importFile = function(file) { 993 var url, sub_template; 994 if ( !this.url && !this.path && this.options.allowInlineIncludes ) { 995 sub_template = Twig.Templates.load(file); 996 sub_template.options = this.options; 997 if ( sub_template ) { 998 return sub_template; 999 } 1000
1001 throw new Twig.Error("Didn't find the inline template by id"); 1002 } 1003 1004 url = relativePath(this, file); 1005 1006 // Load blocks from an external file 1007 sub_template = Twig.Templates.loadRemote(url, { 1008 method: this.url?'ajax':'fs', 1009 base: this.base, 1010 async: false, 1011 options: this.options, 1012 id: url 1013 }); 1014 1015 return sub_template; 1016 }; 1017 1018 Twig.Template.prototype.importBlocks = function(file, override) { 1019 var sub_template = this.importFile(file), 1020 context = this.context, 1021 that = this, 1022 key; 1023 1024 override = override || false; 1025 1026 sub_template.render(context); 1027 1028 // Mixin blocks 1029 Twig.forEach(Object.keys(sub_template.blocks), function(key) { 1030 if (override || that.blocks[key] === undefined) { 1031 that.blocks[key] = sub_template.blocks[key]; 1032 that.importedBlocks.push(key); 1033 } 1034 }); 1035 }; 1036 1037 Twig.Template.prototype.compile = function(options) { 1038 // compile the template into raw JS 1039 return Twig.compiler.compile(this, options); 1040 }; 1041 1042 /** 1043 * Create safe output 1044 * 1045 * @param {string} Content safe to output 1046 * 1047 * @return {String} Content wrapped into a String 1048 */ 1049 1050 Twig.Markup = function(content) { 1051 if (typeof content === 'string' && content.length > 0) { 1052 content = new String(content); 1053 content.twig_markup = true; 1054 } 1055 return content; 1056 } 1057 1058 /** 1059 * Generate the relative canonical version of a url based on the given base path and file path. 1060 * 1061 * @param {string} template The Twig.Template. 1062 * @param {string} file The file path, relative to the base path. 1063 * 1064 * @return {string} The canonical version of the path. 1065 */ 1066 function relativePath(template, file) { 1067 var base, 1068 base_path, 1069 sep_chr = "/", 1070 new_path = [], 1071 val; 1072 1073 if (template.url) { 1074 if (typeof template.base !== 'undefined') { 1075 base = template.base + ((template.base.charAt(template.base.length-1) === '/') ? '' : '/'); 1076 } else { 1077 base = template.url; 1078 } 1079 } else if (template.path) { 1080 // Get the system-specific path separator 1081 var path = require("path"), 1082 sep = path.sep || sep_chr, 1083 relative = new RegExp("^\\.{1,2}" + sep.replace("\\", "\\\\")); 1084 file = file.replace(/\//g, sep); 1085 1086 if (template.base !== undefined && file.match(relative) == null) { 1087 file = file.replace(template.base, ''); 1088 base = template.base + sep; 1089 } else { 1090 base = template.path; 1091 } 1092 1093 base = base.replace(sep+sep, sep); 1094 sep_chr = sep; 1095 } else { 1096 throw new Twig.Error("Cannot extend an inline template."); 1097 } 1098 1099 base_path = base.split(sep_chr); 1100 1101 // Remove file from url 1102 base_path.pop(); 1103 base_path = base_path.concat(file.split(sep_chr)); 1104 1105 while (base_path.length > 0) { 1106 val = base_path.shift(); 1107 if (val == ".") { 1108 // Ignore 1109 } else if (val == ".." && new_path.length > 0 && new_path[new_path.length-1] != "..") { 1110 new_path.pop(); 1111 } else { 1112 new_path.push(val); 1113 } 1114 } 1115 1116 return new_path.join(sep_chr); 1117 } 1118 1119 return Twig; 1120 1121}) (Twig || { }); 1122 1123// The following methods are from MDN and are available under a 1124// [MIT License](http://www.opensource.org/licenses/mit-license.php) or are 1125// [Public Domain](https://developer.mozilla.org/Project:Copyrights). 1126// 1127// See: 1128// * [Object.keys - MDN](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/keys) 1129 1130// ## twig.fills.js 1131// 1132// This file contains fills for backwards compatability. 1133(function() { 1134 "use strict"; 1135 // Handle methods that don't yet exist in every browser 1136 1137 if (!String.prototype.trim) { 1138 String.prototype.trim = function() { 1139 return this.replace(/^\s+|\s+$/g,''); 1140 } 1141 }; 1142 1143 if(!Object.keys) Object.keys = function(o){ 1144 if (o !== Object(o)) { 1145 throw new TypeError('Object.keys called on non-object'); 1146 } 1147 var ret = [], p; 1148 for (p in o) if (Object.prototype.hasOwnProperty.call(o, p)) ret.push(p); 1149 return ret; 1150 } 1151 1152})(); 1153// ## twig.lib.js 1154// 1155// This file contains 3rd party libraries used within twig. 1156// 1157// Copies of the licenses for the code included here can be found in the 1158// LICENSES.md file. 1159// 1160 1161var Twig = (function(Twig) { 1162 1163 // Namespace for libraries 1164 Twig.lib = { }; 1165 1166 /** 1167 sprintf() for JavaScript 1.0.3 1168 https://github.com/alexei/sprintf.js 1169 **/ 1170 var sprintfLib = (function() { 1171 var re = { 1172 not_string: /[^s]/, 1173 number: /[diefg]/, 1174 json: /[j]/, 1175 not_json: /[^j]/, 1176 text: /^[^\x25]+/, 1177 modulo: /^\x25{2}/, 1178 placeholder: /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-gijosuxX])
1178/, 1179 key: /^([a-z_][a-z_\d]*)/i, 1180 key_access: /^\.([a-z_][a-z_\d]*)/i, 1181 index_access: /^\[(\d+)\]/, 1182 sign: /^[\+\-]/ 1183 } 1184 1185 function sprintf() { 1186 var key = arguments[0], cache = sprintf.cache 1187 if (!(cache[key] && cache.hasOwnProperty(key))) { 1188 cache[key] = sprintf.parse(key) 1189 } 1190 return sprintf.format.call(null, cache[key], arguments) 1191 } 1192 1193 sprintf.format = function(parse_tree, argv) { 1194 var cursor = 1, tree_length = parse_tree.length, node_type = "", arg, output = [], i, k, match, pad, pad_character, pad_length, is_positive = true, sign = "" 1195 for (i = 0; i < tree_length; i++) { 1196 node_type = get_type(parse_tree[i]) 1197 if (node_type === "string") { 1198 output[output.length] = parse_tree[i] 1199 } 1200 else if (node_type === "array") { 1201 match = parse_tree[i] // convenience purposes only 1202 if (match[2]) { // keyword argument 1203 arg = argv[cursor] 1204 for (k = 0; k < match[2].length; k++) { 1205 if (!arg.hasOwnProperty(match[2][k])) { 1206 throw new Error(sprintf("[sprintf] property '%s' does not exist", match[2][k])) 1207 } 1208 arg = arg[match[2][k]] 1209 } 1210 } 1211 else if (match[1]) { // positional argument (explicit) 1212 arg = argv[match[1]] 1213 } 1214 else { // positional argument (implicit) 1215 arg = argv[cursor++] 1216 } 1217 1218 if (get_type(arg) == "function") { 1219 arg = arg() 1220 } 1221 1222 if (re.not_string.test(match[8]) && re.not_json.test(match[8]) && (get_type(arg) != "number" && isNaN(arg))) { 1223 throw new TypeError(sprintf("[sprintf] expecting number but found %s", get_type(arg))) 1224 } 1225 1226 if (re.number.test(match[8])) { 1227 is_positive = arg >= 0 1228 } 1229 1230 switch (match[8]) { 1231 case "b": 1232 arg = arg.toString(2) 1233 break 1234 case "c": 1235 arg = String.fromCharCode(arg) 1236 break 1237 case "d": 1238 case "i": 1239 arg = parseInt(arg, 10) 1240 break 1241 case "j": 1242 arg = JSON.stringify(arg, null, match[6] ? parseInt(match[6]) : 0) 1243 break 1244 case "e": 1245 arg = match[7] ? arg.toExponential(match[7]) : arg.toExponential() 1246 break 1247 case "f": 1248 arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg) 1249 break 1250 case "g": 1251 arg = match[7] ? parseFloat(arg).toPrecision(match[7]) : parseFloat(arg) 1252 break 1253 case "o": 1254 arg = arg.toString(8) 1255 break 1256 case "s": 1257 arg = ((arg = String(arg)) && match[7] ? arg.substring(0, match[7]) : arg) 1258 break 1259 case "u": 1260 arg = arg >>> 0 1261 break 1262 case "x": 1263 arg = arg.toString(16) 1264 break 1265 case "X": 1266 arg = arg.toString(16).toUpperCase() 1267 break 1268 } 1269 if (re.json.test(match[8])) { 1270 output[output.length] = arg 1271 } 1272 else { 1273 if (re.number.test(match[8]) && (!is_positive || match[3])) { 1274 sign = is_positive ? "+" : "-" 1275 arg = arg.toString().replace(re.sign, "") 1276 } 1277 else { 1278 sign = "" 1279 } 1280 pad_character = match[4] ? match[4] === "0" ? "0" : match[4].charAt(1) : " " 1281 pad_length = match[6] - (sign + arg).length 1282 pad = match[6] ? (pad_length >
1282 0 ? str_repeat(pad_character, pad_length) : "") : "" 1283 output[output.length] = match[5] ? sign + arg + pad : (pad_character === "0" ? sign + pad + arg : pad + sign + arg) 1284 } 1285 } 1286 } 1287 return output.join("") 1288 } 1289 1290 sprintf.cache = {} 1291 1292 sprintf.parse = function(fmt) { 1293 var _fmt = fmt, match = [], parse_tree = [], arg_names = 0 1294 while (_fmt) { 1295 if ((match = re.text.exec(_fmt)) !== null) { 1296 parse_tree[parse_tree.length] = match[0] 1297 } 1298 else if ((match = re.modulo.exec(_fmt)) !== null) { 1299 parse_tree[parse_tree.length] = "%" 1300 } 1301 else if ((match = re.placeholder.exec(_fmt)) !== null) { 1302 if (match[2]) { 1303 arg_names |= 1 1304 var field_list = [], replacement_field = match[2], field_match = [] 1305 if ((field_match = re.key.exec(replacement_field)) !== null) { 1306 field_list[field_list.length] = field_match[1] 1307 while ((replacement_field = replacement_field.substring(field_match[0].length)) !== "") { 1308 if ((field_match = re.key_access.exec(replacement_field)) !== null) { 1309 field_list[field_list.length] = field_match[1] 1310 } 1311 else if ((field_match = re.index_access.exec(replacement_field)) !== null) { 1312 field_list[field_list.length] = field_match[1] 1313 } 1314 else { 1315 throw new SyntaxError("[sprintf] failed to parse named argument key") 1316 } 1317 } 1318 } 1319 else { 1320 throw new SyntaxError("[sprintf] failed to parse named argument key") 1321 } 1322 match[2] = field_list 1323 } 1324 else { 1325 arg_names |= 2 1326 } 1327 if (arg_names === 3) { 1328 throw new Error("[sprintf] mixing positional and named placeholders is not (yet) supported") 1329 } 1330 parse_tree[parse_tree.length] = match 1331 } 1332 else { 1333 throw new SyntaxError("[sprintf] unexpected placeholder") 1334 } 1335 _fmt = _fmt.substring(match[0].length) 1336 } 1337 return parse_tree 1338 } 1339 1340 var vsprintf = function(fmt, argv, _argv) { 1341 _argv = (argv || []).slice(0) 1342 _argv.splice(0, 0, fmt) 1343 return sprintf.apply(null, _argv) 1344 } 1345 1346 /** 1347 * helpers 1348 */ 1349 function get_type(variable) { 1350 return Object.prototype.toString.call(variable).slice(8, -1).toLowerCase() 1351 } 1352 1353 function str_repeat(input, multiplier) { 1354 return Array(multiplier + 1).join(input) 1355 } 1356 1357 /** 1358 * export 1359 */ 1360 return { 1361 sprintf: sprintf, 1362 vsprintf: vsprintf 1363 } 1364 })(); 1365 1366 var sprintf = sprintfLib.sprintf; 1367 var vsprintf = sprintfLib.vsprintf; 1368 1369 // Expose to Twig 1370 Twig.lib.sprintf = sprintf; 1371 Twig.lib.vsprintf = vsprintf; 1372 1373 1374 /** 1375 * jPaq - A fully customizable JavaScript/JScript library 1376 * http://jpaq.org/ 1377 * 1378 * Copyright (c) 2011 Christopher West 1379 * Licensed under the MIT license. 1380 * http://jpaq.org/license/ 1381 * 1382 * Version: 1.0.6.0000W 1383 * Revised: April 6, 2011 1384 */ 1385 ; (function() { 1386 var shortDays = "Sun,Mon,Tue,Wed,Thu,Fri,Sat".split(","); 1387 var fullDays = "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday".split(","); 1388 var shortMonths = "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec".split(","); 1389 var fullMonths = "January,February,March,April,May,June,July,August,September,October,November,December".split(","); 1390 function getOrdinalFor(intNum) { 1391 return (((intNum = Math.abs(intNum) % 100) % 10 == 1 && intNum != 11) ? "st" 1392 : (intNum % 10 == 2 && intNum != 12) ? "nd" : (intNum % 10 == 3 1393 && intNum != 13) ? "rd" : "th"); 1394 } 1395 function getISO8601Year(aDate) { 1396 var d = new Date(aDate.getFullYear() + 1, 0, 4); 1397 if((d - aDate) / 86400000 < 7 && (aDate.getDay() + 6) % 7 < (d.getDay() + 6) % 7) 1398 return d.getFullYear(); 1399 if(aDate.getMonth() > 0 || aDate.getDate() >= 4) 1400 return aDate.getFullYear(); 1401 return aDate.getFullYear() - (((aDate.getDay() + 6) % 7 - aDate.getDate() > 2) ? 1 : 0); 1402 } 1403 function getISO8601Week(aDate) { 1404 // Get a day during the first week of the year. 1405 var d = new Date(getISO8601Year(aDate), 0, 4); 1406 // Get the first monday of the year. 1407 d.setDate(d.getDate() - (d.getDay() + 6) % 7); 1408 return parseInt((aDate - d) / 604800000) + 1; 1409 } 1410 Twig.lib.formatDate = function(date, format) { 1411 /// <summary> 1412 /// Gets a string for this date, formatted according to the given format 1413 /// string. 1414 /// </summary> 1415 /// <param name="format" type="String"> 1416 /// The format of the output date string. The format str
1416ing works in a 1417 /// nearly identical way to the PHP date function which is highlighted here: 1418 /// http://php.net/manual/en/function.date.php. 1419 /// The only difference is the fact that "u" signifies milliseconds 1420 /// instead of microseconds. The following characters are recognized in 1421 /// the format parameter string: 1422 /// d - Day of the month, 2 digits with leading zeros 1423 /// D - A textual representation of a day, three letters 1424 /// j - Day of the month without leading zeros 1425 /// l (lowercase 'L') - A full textual representation of the day of the week 1426 /// N - ISO-8601 numeric representation of the day of the week (starting from 1) 1427 /// S - English ordinal suffix for the day of the month, 2 characters st, 1428 /// nd, rd or th. Works well with j. 1429 /// w - Numeric representation of the day of the week (starting from 0) 1430 /// z - The day of the year (starting from 0) 1431 /// W - ISO-8601 week number of year, weeks starting on Monday 1432 /// F - A full textual representation of a month, such as January or March 1433 /// m - Numeric representation of a month, with leading zeros 1434 /// M - A short textual representation of a month, three letters 1435 /// n - Numeric representation of a month, without leading zeros 1436 /// t - Number of days in the given month 1437 /// L - Whether it's a leap year 1438 /// o - ISO-8601 year number. This has the same value as Y, except that if 1439 /// the ISO week number (W) belongs to the previous or next year, that 1440 /// year is used instead. 1441 /// Y - A full numeric representation of a year, 4 digits 1442 /// y - A two digit representation of a year 1443 /// a - Lowercase Ante meridiem and Post meridiem 1444 /// A - Uppercase Ante meridiem and Post meridiem 1445 /// B - Swatch Internet time 1446 /// g - 12-hour format of an hour without leading zeros 1447 /// G - 24-hour format of an hour without leading zeros 1448 /// h - 12-hour format of an hour with leading zeros 1449 /// H - 24-hour format of an hour with leading zeros 1450 /// i - Minutes with leading zeros 1451 /// s - Seconds, with leading zeros 1452 /// u - Milliseconds 1453 /// U - Seconds since the Unix Epoch (January 1 1970 00:00:00 GMT) 1454 /// </param> 1455 /// <returns type="String"> 1456 /// Returns the string for this date, formatted according to the given 1457 /// format string. 1458 /// </returns> 1459 // If the format was not passed, use the default toString method. 1460 if(typeof format !== "string" || /^\s*$/.test(format)) 1461 return date + ""; 1462 var jan1st = new Date(date.getFullYear(), 0, 1); 1463 var me = date; 1464 return format.replace(/[dDjlNSwzWFmMntLoYyaABgGhHisuU]/g, function(option) { 1465 switch(option) { 1466 // Day of the month, 2 digits with leading zeros 1467 case "d": return ("0" + me.getDate()).replace(/^.+(..)$/, "$1"); 1468 // A textual representation of a day, three letters 1469 case "D": return shortDays[me.getDay()]; 1470 // Day of the month without leading zeros 1471 case "j": return me.getDate(); 1472 // A full textual representation of the day of the week 1473 case "l": return fullDays[me.getDay()]; 1474 // ISO-8601 numeric representation of the day of the week 1475 case "N": return (me.getDay() + 6) % 7 + 1; 1476 // English ordinal suffix for the day of the month, 2 characters 1477 case "S": return getOrdinalFor(me.getDate()); 1478 // Numeric representation of the day of the week 1479 case "w": return me.getDay(); 1480 // The day of the year (starting from 0) 1481 case "z": return Math.ceil((jan1st - me) / 86400000); 1482 // ISO-8601 week number of year, weeks starting on Monday 1483 case "W": return ("0" + getISO8601Week(me)).replace(/^.(..)$/, "$1"); 1484 // A full textual representation of a month, such as January or March 1485 case "F": return fullMonths[me.getMonth()]; 1486 // Numeric representation of a month, with leading zeros 1487 case "m": return ("0" + (me.getMonth() + 1)).replace(/^.+(..)$/, "$1"); 1488 // A short textual representation of a month, three letters 1489 case "M": return shortMonths[me.getMonth()]; 1490 // Numeric representation of a month, without leading zeros 1491 case "n": return me.getMonth() + 1; 1492 // Number of days in the given month 1493 case "t": return new Date(me.getFullYear(), me.getMonth() + 1, -1).getDate(); 1494 // Whether it's a leap year 1495 case "L": return new Date(me.getFullYear(), 1, 29).getDate() == 29 ? 1 : 0; 1496 // ISO-8601 year number. This has the same value as Y, except that if the 1497 // ISO week number (W) belongs to the previous or next year, that year is 1498 // used instead. 1499 case "o": return getISO8601Year(me); 1500 // A full numeric representation of a year, 4 digits 1501 case "Y": return me.getFullYear(); 1502 // A two digit representation of a year 1503 case "y": return (me.getFullYear() + "").replace(/^.+(..)$/, "$1"); 1504 // Lowercase Ante meridiem and Post meridiem 1505 case "a": return me.getHours() < 12 ? "am" : "pm"; 1506 // Uppercase Ante meridiem and Post meridiem 1507 case "A": return me.getHours() < 12 ? "AM" : "PM"; 1508 // Swatch Internet time 1509 case "B": return Math.floor((((me.getUTCHours() + 1) % 24) + me.getUTCMinutes() / 60 + me.getUTCSeconds() / 3600) * 1000 / 24); 1510 // 12-hour format of an hour without leading zeros 1511 case "g": return me.getHours() % 12 != 0 ? me.getHours() % 12 : 12; 1512 // 24-hour format of an hour without leading zeros 1513 case "G": return me.getHours(); 1514 // 12-hour format of an hour with leading zeros 1515 case "h": return ("0" + (me.getHours() % 12 != 0 ? me.getHours() % 12 : 12)).replace(/^.+(..)$/, "$1"); 1516 // 24-hour format of an hour with leading zeros 1517 case "H": return ("0" + me.getHours()).replace(/^.+(..)$/, "$1"); 1518 // Minutes with leading zeros 1519 case "i": return ("0" + me.getMinutes()).replace(/^.+(..)$/, "$1"); 1520 // Seconds, with leading zeros 1521 case "s": return ("0" + me.getSeconds()).replace(/^.+(..)$/, "$1"); 1522 // Milliseconds 1523 case "u": return me.getMilliseconds(); 1524 // Seconds since the Unix Epoch (January 1 1970 00:00:00 GMT) 1525 case "U": return me.getTime() / 1000; 1526 } 1527 }); 1528 }; 1529 })(); 1530 1531 Twig.lib.strip_tags = function(input, allowed) { 1532 // Strips HTML and PHP tags from a string 1533 // 1534 // version: 1109.2015 1535 // discuss at: http://phpjs.org/functions/strip_tags 1536 // + original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 1537 // + improved by: Luke Godfrey 1538 // + input by: Pul 1539 // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 1540 // + bugfixed by: Onno Marsman 1541 // + input by: Alex 1542 // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 1543 // + input by: Marc Palau 1544 // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 1545 // + input by: Brett Zamir (http://brett-zamir.me) 1546 // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 1547 // + bugfixed by: Eric Nagel 1548 // + input by: Bobby Drake 1549 // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 1550 // + bugfixed by: Tomasz Wesolowski 1551 // + input by: Evertjan Garretsen 1552 // + revised by: Rafał Kukawski (http://blog.kukawski.pl/) 1553 // * example 1: strip_tags('<p>Kevin</p> <b>van</b> <i>Zonneveld</i>', '<i><b>'); 1554 // * returns 1: 'Kevin <b>van</b> <i>Zonneveld</i>' 1555 // * example 2: strip_tags('<p>Kevin <img src="someimage.png" onmouseover="someFunction()">van <i>Zonneveld</i></p>', '<p>'); 1556 // * returns 2: '<p>Kevin van Zonneveld</p>' 1557 // * example 3: strip_tags("<a href='http://kevin.vanzonneveld.net'>Kevin van Zonneveld</a>", "<a>"); 1558 // * returns 3: '<a href='http://kevin.vanzonneveld.net'>Kevin van Zonneveld</a>' 1559 // * example 4: strip_tags('1 < 5 5 > 1'); 1560 // * returns 4: '1 < 5 5 > 1'
1561 // * example 5: strip_tags('1 <br/> 1'); 1562 // * returns 5: '1 1' 1563 // * example 6: strip_tags('1 <br/> 1', '<br>'); 1564 // * returns 6: '1 1' 1565 // * example 7: strip_tags('1 <br/> 1', '<br><br/>'); 1566 // * returns 7: '1 <br/> 1' 1567 allowed = (((allowed || "") + "").toLowerCase().match(/<[a-z][a-z0-9]*>/g) || []).join(''); // making sure the allowed arg is a string containing only tags in lowercase (<a><b><c>) 1568 var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi, 1569 commentsAndPhpTags = /<!--[\s\S]*?-->|<\?(?:php)?[\s\S]*?\?>/gi; 1570 return input.replace(commentsAndPhpTags, '').replace(tags, function ($0, $1) { 1571 return allowed.indexOf('<' + $1.toLowerCase() + '>') > -1 ? $0 : ''; 1572 }); 1573 } 1574 1575 Twig.lib.parseISO8601Date = function (s){ 1576 // Taken from http://n8v.enteuxis.org/2010/12/parsing-iso-8601-dates-in-javascript/ 1577 // parenthese matches: 1578 // year month day hours minutes seconds 1579 // dotmilliseconds 1580 // tzstring plusminus hours minutes 1581 var re = /(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(\.\d+)?(Z|([+-])(\d\d):(\d\d))/; 1582 1583 var d = []; 1584 d = s.match(re); 1585 1586 // "2010-12-07T11:00:00.000-09:00" parses to: 1587 // ["2010-12-07T11:00:00.000-09:00", "2010", "12", "07", "11", 1588 // "00", "00", ".000", "-09:00", "-", "09", "00"] 1589 // "2010-12-07T11:00:00.000Z" parses to: 1590 // ["2010-12-07T11:00:00.000Z", "2010", "12", "07", "11", 1591 // "00", "00", ".000", "Z", undefined, undefined, undefined] 1592 1593 if (! d) { 1594 throw "Couldn't parse ISO 8601 date string '" + s + "'"; 1595 } 1596 1597 // parse strings, leading zeros into proper ints 1598 var a = [1,2,3,4,5,6,10,11]; 1599 for (var i in a) { 1600 d[a[i]] = parseInt(d[a[i]], 10); 1601 } 1602 d[7] = parseFloat(d[7]); 1603 1604 // Date.UTC(year, month[, date[, hrs[, min[, sec[, ms]]]]]) 1605 // note that month is 0-11, not 1-12 1606 // see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Date/UTC 1607 var ms = Date.UTC(d[1], d[2] - 1, d[3], d[4], d[5], d[6]); 1608 1609 // if there are milliseconds, add them 1610 if (d[7] > 0) { 1611 ms += Math.round(d[7] * 1000); 1612 } 1613 1614 // if there's a timezone, calculate it 1615 if (d[8] != "Z" && d[10]) { 1616 var offset = d[10] * 60 * 60 * 1000; 1617 if (d[11]) { 1618 offset += d[11] * 60 * 1000; 1619 } 1620 if (d[9] == "-") { 1621 ms -= offset; 1622 } 1623 else { 1624 ms += offset; 1625 } 1626 } 1627 1628 return new Date(ms); 1629 }; 1630 1631 Twig.lib.strtotime = function (text, now) { 1632 // discuss at: http://phpjs.org/functions/strtotime/ 1633 // version: 1109.2016 1634 // original by: Caio Ariede (http://caioariede.com) 1635 // improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 1636 // improved by: Caio Ariede (http://caioariede.com) 1637 // improved by: A. MatÃÂas Quezada (http://amatiasq.com) 1638 // improved by: preuter 1639 // improved by: Brett Zamir (http://brett-zamir.me) 1640 // improved by: Mirko Faber 1641 // input by: David 1642 // bugfixed by: Wagner B. Soares 1643 // bugfixed by: Artur Tchernychev 1644 // note: Examples all have a fixed timestamp to prevent tests to fail because of variable time(zones) 1645 // example 1: strtotime('+1 day', 1129633200); 1646 // returns 1: 1129719600 1647 // example 2: strtotime('+1 week 2 days 4 hours 2 seconds', 1129633200); 1648 // returns 2: 1130425202 1649 // example 3: strtotime('last month', 1129633200); 1650 // returns 3: 1127041200 1651 // example 4: strtotime('2009-05-04 08:30:00 GMT'); 1652 // returns 4: 1241425800 1653 1654 var parsed, match, today, year, date, days, ranges, len, times, regex, i, fail = false; 1655 1656 if (!text) { 1657 return fail; 1658 } 1659 1660 // Unecessary spaces 1661 text = text.replace(/^\s+|\s+$/g, '') 1662 .replace(/\s{2,}/g, ' ') 1663 .replace(/[\t\r\n]/g, '') 1664 .toLowerCase(); 1665 1666 // in contrast to php, js Date.parse function interprets: 1667 // dates given as yyyy-mm-dd as in timezone: UTC, 1668 // dates with "." or "-" as MDY instead of DMY 1669 // dates with two-digit years differently 1670 // etc...etc... 1671 // ...therefore we manually parse lots of common date formats 1672 match = text.match( 1673 /^(\d{1,4})([\-\.\/\:])(\d{1,2})([\-\.\/\:])(\d{1,4})(?:\s(\d{1,2}):(\d{2})?:?(\d{2})?)?(?:\s([A-Z]+)?)?$/); 1674 1675 if (match && match[2] === match[4]) { 1676 if (match[1] > 1901) { 1677 switch (match[2]) { 1678 case '-': 1679 { 1680 // YYYY-M-D 1681 if (match[3] > 12 || match[5] > 31) { 1682 return fail; 1683 } 1684 1685 return new Date(match[1], parseInt(match[3], 10) - 1, match[5], 1686 match[6] || 0, match[7] || 0, match[8] || 0, match[9] || 0) / 1000; 1687 } 1688 case '.': 1689 { 1690 // YYYY.M.D is not parsed by strtotime() 1691 return fail; 1692 } 1693 case '/': 1694 { 1695 // YYYY/M/D 1696 if (match[3] > 12 || match[5] > 31) { 1697 return fail; 1698 } 1699 1700 return new Date(match[1], parseInt(match[3], 10) - 1, match[5], 1701 match[6] || 0, match[7] || 0, match[8] || 0, match[9] || 0) / 1000; 1702 } 1703 } 1704 } else if (match[5] > 1901) { 1705 switch (match[2]) { 1706 case '-': 1707 { 1708 // D-M-YYYY 1709 if (match[3] > 12 || match[1] > 31) { 1710 return fail; 1711 } 1712 1713 return new Date(match[5], parseInt(match[3], 10) - 1, match[1], 1714 match[6] || 0, match[7] || 0, match[8] || 0, match[9] || 0) / 1000; 1715 } 1716 case '.': 1717 { 1718 // D.M.YYYY 1719 if (match[3] > 12 || match[1] > 31) { 1720 return fail; 1721 } 1722 1723 return new Date(match[5], parseInt(match[3], 10) - 1, match[1], 1724 match[6] || 0, match[7] || 0, match[8] || 0, match[9] || 0) / 1000; 1725 } 1726 case '/': 1727 { 1728 // M/D/YYYY 1729 if (match[1] > 12 || match[3] > 31) { 1730 return fail; 1731 } 1732 1733 return new Date(match[5], parseInt(match[1], 10) - 1, match[3], 1734 match[6] || 0, match[7] || 0, match[8] || 0, match[9] || 0) / 1000; 1735 } 1736 } 1737 } else { 1738 switch (match[2]) { 1739 case '-': 1740 { 1741 // YY-M-D 1742 if (match[3] > 12 || match[5] > 31 || (match[1] < 70 && match[1] > 38)) { 1743 return fail; 1744 } 1745 1746 year = match[1] >= 0 && match[1] <= 38 ? +match[1] + 2000 : match[1]; 1747 return new Date(year, parseInt(match[3], 10) - 1, match[5], 1748 match[6] || 0, match[7] || 0, match[8] || 0, match[9] || 0) / 1000; 1749 } 1750 case '.': 1751 { 1752 // D.M.YY or H.MM.SS 1753 if (match[5] >= 70) { 1754 // D.M.YY 1755 if (match[3] > 12 || match[1] > 31) { 1756 return fail; 1757 } 1758 1759 return new Date(match[5], parseInt(match[3], 10) - 1, match[1], 1760 match[6] || 0, match[7] || 0, match[8] || 0, match[9] || 0) / 1000; 1761 } 1762 if (match[5] < 60 && !match[6]) { 1763 // H.MM.SS 1764 if (match[1] > 23 || match[3] > 59) { 1765 return fail; 1766 } 1767 1768 today = new Date(); 1769 return new Date(today.getFullYear(), today.getMonth(), today.getDate(), 1770 match[1] || 0, match[3] || 0, match[5] || 0, match[9] || 0) / 1000; 1771 } 1772 1773 // invalid format, cannot be parsed 1774 return fail; 1775 } 1776 case '/': 1777 { 1778 // M/D/YY 1779 if (match[1] > 12 || match[3] > 31 || (match[5] < 70 && match[5] > 38)) { 1780 return fail; 1781 } 1782 1783 year = match[5] >= 0 && match[5] <= 38 ? +match[5] + 2000 : match[5]; 1784 return new Date(year, parseInt(match[1], 10) - 1, match[3], 1785 match[6] || 0, match[7] || 0, match[8] || 0, match[9] || 0) / 1000; 1786 } 1787 case ':': 1788 { 1789 // HH:MM:SS 1790 if (match[1] > 23 || match[3] > 59 || match[5] > 59) { 1791 return fail; 1792 } 1793 1794 today = new Date(); 1795 return new Date(today.getFullYear(), today.getMonth(), today.getDate(), 1796 match[1] || 0, match[3] || 0, match[5] || 0) / 1000; 1797 } 1798 } 1799 } 1800 } 1801 1802 // other formats and "now" should be parsed by Date.parse() 1803 if (text === 'now') { 1804 return now === null || isNaN(now) ? new Date() 1805 .getTime() / 1000 | 0 : now | 0; 1806 } 1807 if (!isNaN(parsed = Date.parse(text))) { 1808 return parsed / 1000 | 0; 1809 } 1810 1811 date = now ? new Date(now * 1000) : new Date(); 1812 days = { 1813 'sun': 0, 1814 'mon': 1, 1815 'tue': 2, 1816 'wed': 3, 1817 'thu': 4, 1818 'fri': 5,
1819 'sat': 6 1820 }; 1821 ranges = { 1822 'yea': 'FullYear', 1823 'mon': 'Month', 1824 'day': 'Date', 1825 'hou': 'Hours', 1826 'min': 'Minutes', 1827 'sec': 'Seconds' 1828 }; 1829 1830 function lastNext(type, range, modifier) { 1831 var diff, day = days[range]; 1832 1833 if (typeof day !== 'undefined') { 1834 diff = day - date.getDay(); 1835 1836 if (diff === 0) { 1837 diff = 7 * modifier; 1838 } else if (diff > 0 && type === 'last') { 1839 diff -= 7; 1840 } else if (diff < 0 && type === 'next') { 1841 diff += 7; 1842 } 1843 1844 date.setDate(date.getDate() + diff); 1845 } 1846 } 1847 1848 function process(val) { 1849 var splt = val.split(' '), // Todo: Reconcile this with regex using \s, taking into account browser issues with split and regexes 1850 type = splt[0], 1851 range = splt[1].substring(0, 3), 1852 typeIsNumber = /\d+/.test(type), 1853 ago = splt[2] === 'ago', 1854 num = (type === 'last' ? -1 : 1) * (ago ? -1 : 1); 1855 1856 if (typeIsNumber) { 1857 num *= parseInt(type, 10); 1858 } 1859 1860 if (ranges.hasOwnProperty(range) && !splt[1].match(/^mon(day|\.)?$/i)) { 1861 return date['set' + ranges[range]](date['get' + ranges[range]]() + num); 1862 } 1863 1864 if (range === 'wee') { 1865 return date.setDate(date.getDate() + (num * 7)); 1866 } 1867 1868 if (type === 'next' || type === 'last') { 1869 lastNext(type, range, num); 1870 } else if (!typeIsNumber) { 1871 return false; 1872 } 1873 1874 return true; 1875 } 1876 1877 times = '(years?|months?|weeks?|days?|hours?|minutes?|min|seconds?|sec' + 1878 '|sunday|sun\\.?|monday|mon\\.?|tuesday|tue\\.?|wednesday|wed\\.?' + 1879 '|thursday|thu\\.?|friday|fri\\.?|saturday|sat\\.?)'; 1880 regex = '([+-]?\\d+\\s' + times + '|' + '(last|next)\\s' + times + ')(\\sago)?'; 1881 1882 match = text.match(new RegExp(regex, 'gi')); 1883 if (!match) { 1884 return fail; 1885 } 1886 1887 for (i = 0, len = match.length; i < len; i++) { 1888 if (!process(match[i])) { 1889 return fail; 1890 } 1891 } 1892 1893 // ECMAScript 5 only 1894 // if (!match.every(process)) 1895 // return false; 1896 1897 return (date.getTime() / 1000); 1898 }; 1899 1900 Twig.lib.is = function(type, obj) { 1901 var clas = Object.prototype.toString.call(obj).slice(8, -1); 1902 return obj !== undefined && obj !== null && clas === type; 1903 }; 1904 1905 // shallow-copy an object 1906 Twig.lib.copy = function(src) { 1907 var target = {}, 1908 key; 1909 for (key in src) 1910 target[key] = src[key]; 1911 1912 return target; 1913 }; 1914 1915 Twig.lib.replaceAll = function(string, search, replace) { 1916 return string.split(search).join(replace); 1917 }; 1918 1919 // chunk an array (arr) into arrays of (size) items, returns an array of arrays, or an empty array on invalid input 1920 Twig.lib.chunkArray = function (arr, size) { 1921 var returnVal = [], 1922 x = 0, 1923 len = arr.length; 1924 1925 if (size < 1 || !Twig.lib.is("Array", arr)) { 1926 return []; 1927 } 1928 1929 while (x < len) { 1930 returnVal.push(arr.slice(x, x += size)); 1931 } 1932 1933 return returnVal; 1934 }; 1935 1936 Twig.lib.round = function round(value, precision, mode) { 1937 // discuss at: http://phpjs.org/functions/round/ 1938 // original by: Philip Peterson 1939 // revised by: Onno Marsman 1940 // revised by: T.Wild 1941 // revised by: Rafał Kukawski (http://blog.kukawski.pl/) 1942 // input by: Greenseed 1943 // input by: meo 1944 // input by: William 1945 // input by: Josep Sanz (http://www.ws3.es/) 1946 // bugfixed by: Brett Zamir (http://brett-zamir.me) 1947 // note: Great work. Ideas for improvement: 1948 // note: - code more compliant with developer guidelines 1949 // note: - for implementing PHP constant arguments look at 1950 // note: the pathinfo() function, it offers the greatest 1951 // note: flexibility & compatibility possible 1952 // example 1: round(1241757, -3); 1953 // returns 1: 1242000 1954 // example 2: round(3.6); 1955 // returns 2: 4 1956 // example 3: round(2.835, 2); 1957 // returns 3: 2.84 1958 // example 4: round(1.1749999999999, 2); 1959 // returns 4: 1.17 1960 // example 5: round(58551.799999999996, 2); 1961 // returns 5: 58551.8 1962 1963 var m, f, isHalf, sgn; // helper variables 1964 precision |= 0; // making sure precision is integer 1965 m = Math.pow(10, precision); 1966 value *= m; 1967 sgn = (value > 0) | -(value < 0); // sign of the number 1968 isHalf = value % 1 === 0.5 * sgn; 1969 f = Math.floor(value); 1970 1971 if (isHalf) { 1972 switch (mode) { 1973 case 'PHP_ROUND_HALF_DOWN': 1974 value = f + (sgn < 0); // rounds .5 toward zero 1975 break; 1976 case 'PHP_ROUND_HALF_EVEN': 1977 value = f + (f % 2 * sgn); // rouds .5 towards the next even integer 1978 break; 1979 case 'PHP_ROUND_HALF_ODD': 1980 value = f + !(f % 2); // rounds .5 towards the next odd integer 1981 break; 1982 default: 1983 value = f + (sgn > 0); // rounds .5 away from zero 1984 } 1985 } 1986 1987 return (isHalf ? value : Math.round(value)) / m; 1988 } 1989 1990 return Twig; 1991 1992})(Twig || { }); 1993// Twig.js 1994// Available under the BSD 2-Clause License 1995// https://github.com/justjohn/twig.js 1996 1997// ## twig.logic.js 1998// 1999// This file handles tokenizing, compiling and parsing logic tokens. {% ... %} 2000var Twig = (function (Twig) { 2001 "use strict"; 2002 2003 /** 2004 * Namespace for logic handling. 2005 */ 2006 Twig.logic = {}; 2007 2008 /** 2009 * Logic token types. 2010 */ 2011 Twig.logic.type = { 2012 if_: 'Twig.logic.type.if', 2013 endif: 'Twig.logic.type.endif', 2014 for_: 'Twig.logic.type.for', 2015 endfor: 'Twig.logic.type.endfor', 2016 else_: 'Twig.logic.type.else', 2017 elseif: 'Twig.logic.type.elseif', 2018 set: 'Twig.logic.type.set', 2019 setcapture:'Twig.logic.type.setcapture', 2020 endset: 'Twig.logic.type.endset', 2021 filter: 'Twig.logic.type.filter', 2022 endfilter: 'Twig.logic.type.endfilter', 2023 block: 'Twig.logic.type.block', 2024 endblock: 'Twig.logic.type.endblock', 2025 extends_: 'Twig.logic.type.extends', 2026 use: 'Twig.logic.type.use', 2027 include: 'Twig.logic.type.include', 2028 spaceless: 'Twig.logic.type.spaceless', 2029 endspaceless: 'Twig.logic.type.endspaceless', 2030 macro: 'Twig.logic.type.macro', 2031 endmacro: 'Twig.logic.type.endmacro', 2032 import_: 'Twig.logic.type.import', 2033 from: 'Twig.logic.type.from', 2034 embed: 'Twig.logic.type.embed', 2035 endembed: 'Twig.logic.type.endembed' 2036 }; 2037 2038 2039 // Regular expressions for handling logic tokens. 2040 // 2041 // Properties: 2042 // 2043 // type: The type of expression this matches 2044 // 2045 // regex: A regular expression that matches the format of the token 2046 // 2047 // next: What logic tokens (if any) pop this token off the logic stack. If empty, the 2048 // logic token is assumed to not require an end tag and isn't push onto the stack. 2049 // 2050 // open: Does this tag open a logic expression or is it standalone. For example, 2051 // {% endif %} cannot exist without an opening {% if ... %} tag, so open = false. 2052 // 2053 // Functions: 2054 // 2055 // compile: A function that handles compiling the token into an output token ready for 2056 // parsing with the parse function. 2057 // 2058 // parse: A function that parses the compiled token into output (HTML / whatever the 2059 // template represents). 2060 Twig.logic.definitions = [ 2061 { 2062 /** 2063 * If type logic tokens. 2064 * 2065 * Format: {% if expression %} 2066 */ 2067 type: Twig.logic.type.if_, 2068 regex: /^if\s+([\s\S]+)$/, 2069 next: [ 2070 Twig.logic.type.else_, 2071 Twig.logic.type.elseif, 2072 Twig.logic.type.endif 2073 ], 2074 open: true, 2075 compile: function (token) { 2076 var expression = token.match[1]; 2077 // Compile the expression. 2078 token.stack = Twig.expression.compile.apply(this, [{ 2079 type: Twig.expression.type.expression, 2080 value: expression 2081 }]).stack; 2082 delete token.match; 2083 return token; 2084 }, 2085 parse: function (token, context, chain) { 2086 var output = '', 2087 // Parse the expression 2088 result = Twig.expression.parse.apply(this, [token.stack, context]); 2089 2090 // Start a new logic chain 2091 chain = true; 2092 2093 if (result) { 2094 chain = false;
2095 // parse if output 2096 output = Twig.parse.apply(this, [token.output, context]); 2097 } 2098 return { 2099 chain: chain, 2100 output: output 2101 }; 2102 } 2103 }, 2104 { 2105 /** 2106 * Else if type logic tokens. 2107 * 2108 * Format: {% elseif expression %} 2109 */ 2110 type: Twig.logic.type.elseif, 2111 regex: /^elseif\s+([^\s].*)$/, 2112 next: [ 2113 Twig.logic.type.else_, 2114 Twig.logic.type.elseif, 2115 Twig.logic.type.endif 2116 ], 2117 open: false, 2118 compile: function (token) { 2119 var expression = token.match[1]; 2120 // Compile the expression. 2121 token.stack = Twig.expression.compile.apply(this, [{ 2122 type: Twig.expression.type.expression, 2123 value: expression 2124 }]).stack; 2125 delete token.match; 2126 return token; 2127 }, 2128 parse: function (token, context, chain) { 2129 var output = ''; 2130 2131 if (chain && Twig.expression.parse.apply(this, [token.stack, context]) === true) { 2132 chain = false; 2133 // parse if output 2134 output = Twig.parse.apply(this, [token.output, context]); 2135 } 2136 2137 return { 2138 chain: chain, 2139 output: output 2140 }; 2141 } 2142 }, 2143 { 2144 /** 2145 * Else if type logic tokens. 2146 * 2147 * Format: {% elseif expression %} 2148 */ 2149 type: Twig.logic.type.else_, 2150 regex: /^else$/, 2151 next: [ 2152 Twig.logic.type.endif, 2153 Twig.logic.type.endfor 2154 ], 2155 open: false, 2156 parse: function (token, context, chain) { 2157 var output = ''; 2158 if (chain) { 2159 output = Twig.parse.apply(this, [token.output, context]); 2160 } 2161 return { 2162 chain: chain, 2163 output: output 2164 }; 2165 } 2166 }, 2167 { 2168 /** 2169 * End if type logic tokens. 2170 * 2171 * Format: {% endif %} 2172 */ 2173 type: Twig.logic.type.endif, 2174 regex: /^endif$/, 2175 next: [ ], 2176 open: false 2177 }, 2178 { 2179 /** 2180 * For type logic tokens. 2181 * 2182 * Format: {% for expression %} 2183 */ 2184 type: Twig.logic.type.for_, 2185 regex: /^for\s+([a-zA-Z0-9_,\s]+)\s+in\s+([^\s].*?)(?:\s+if\s+([^\s].*))?$/, 2186 next: [ 2187 Twig.logic.type.else_, 2188 Twig.logic.type.endfor 2189 ], 2190 open: true, 2191 compile: function (token) { 2192 var key_value = token.match[1], 2193 expression = token.match[2], 2194 conditional = token.match[3], 2195 kv_split = null; 2196 2197 token.key_var = null; 2198 token.value_var = null; 2199 2200 if (key_value.indexOf(",") >= 0) { 2201 kv_split = key_value.split(','); 2202 if (kv_split.length === 2) { 2203 token.key_var = kv_split[0].trim(); 2204 token.value_var = kv_split[1].trim(); 2205 } else { 2206 throw new Twig.Error("Invalid expression in for loop: " + key_value); 2207 } 2208 } else { 2209 token.value_var = key_value; 2210 } 2211 2212 // Valid expressions for a for loop 2213 // for item in expression 2214 // for key,item in expression 2215 2216 // Compile the expression. 2217 token.expression = Twig.expression.compile.apply(this, [{ 2218 type: Twig.expression.type.expression, 2219 value: expression 2220 }]).stack; 2221 2222 // Compile the conditional (if available) 2223 if (conditional) { 2224 token.conditional = Twig.expression.compile.apply(this, [{ 2225 type: Twig.expression.type.expression, 2226 value: conditional 2227 }]).stack; 2228 } 2229 2230 delete token.match; 2231 return token; 2232 }, 2233 parse: function (token, context, continue_chain) { 2234 // Parse expression 2235 var result = Twig.expression.parse.apply(this, [token.expression, context]), 2236 output = [], 2237 len, 2238 index = 0, 2239 keyset, 2240 that = this, 2241 conditional = token.conditional, 2242 buildLoop = function(index, len) { 2243 var isConditional = conditional !== undefined; 2244 return { 2245 index: index+1, 2246 index0: index, 2247 revindex: isConditional?undefined:len-index, 2248 revindex0: isConditional?undefined:len-index-1, 2249 first: (index === 0), 2250 last: isConditional?undefined:(index === len-1), 2251 length: isConditional?undefined:len, 2252 parent: context 2253 }; 2254 }, 2255 // run once for each iteration of the loop 2256 loop = function(key, value) { 2257 var inner_context = Twig.ChildContext(context); 2258 2259 inner_context[token.value_var] = value; 2260 2261 if (token.key_var) { 2262 inner_context[token.key_var] = key; 2263 } 2264 2265 // Loop object 2266 inner_context.loop = buildLoop(index, len); 2267 2268 if (conditional === undefined || 2269 Twig.expression.parse.apply(that, [conditional, inner_context])) 2270 { 2271 output.push(Twig.parse.apply(that, [token.output, inner_context])); 2272 index += 1; 2273 } 2274 2275 // Delete loop-related variables from the context 2276 delete inner_context['loop']; 2277 delete inner_context[token.value_var]; 2278 delete inner_context[token.key_var]; 2279 2280 // Merge in values that exist in context but have changed 2281 // in inner_context. 2282 Twig.merge(context, inner_context, true); 2283 }; 2284 2285 2286 if (Twig.lib.is('Array', result)) { 2287 len = result.length; 2288 Twig.forEach(result, function (value) { 2289 var key = index; 2290 2291 loop(key, value); 2292 }); 2293 } else if (Twig.lib.is('Object', result)) { 2294 if (result._keys !== undefined) { 2295 keyset = result._keys; 2296 } else { 2297 keyset = Object.keys(result); 2298 } 2299 len = keyset.length; 2300 Twig.forEach(keyset, function(key) { 2301 // Ignore the _keys property, it's internal to twig.js 2302 if (key === "_keys") return; 2303 2304 loop(key, result[key]); 2305 }); 2306 } 2307 2308 // Only allow else statements if no output was generated 2309 continue_chain = (output.length === 0); 2310 2311 return { 2312 chain: continue_chain, 2313 output: Twig.output.apply(this, [output]) 2314 }; 2315 } 2316 }, 2317 { 2318 /** 2319 * End if type logic tokens. 2320 * 2321 * Format: {% endif %} 2322 */ 2323 type: Twig.logic.type.endfor, 2324 regex: /^endfor$/, 2325 next: [ ], 2326 open: false 2327 }, 2328 { 2329 /** 2330 * Set type logic tokens. 2331 * 2332 * Format: {% set key = expression %} 2333 */ 2334 type: Twig.logic.type.set, 2335 regex: /^set\s+([a-zA-Z0-9_,\s]+)\s*=\s*([\s\S]+)$/, 2336 next: [ ], 2337 open: true, 2338 compile: function (token) { 2339 var key = token.match[1].trim(), 2340 expression = token.match[2], 2341 // Compile the expression. 2342 expression_stack = Twig.expression.compile.apply(this, [{ 2343 type: Twig.expression.type.expression, 2344 value: expression 2345 }]).stack; 2346 2347 token.key = key; 2348 token.expression = expression_stack; 2349 2350 delete token.match; 2351 return token; 2352 }, 2353 parse: function (token, context, continue_chain) { 2354 var value = Twig.expression.parse.apply(this, [token.expression, context]), 2355 key = token.key; 2356 2357 context[key] = value; 2358 2359 return { 2360 chain: continue_chain, 2361 context: context 2362 }; 2363 } 2364 }, 2365 { 2366 /** 2367 * Set capture type logic tokens. 2368 * 2369 * Format: {% set key %} 2370 */ 2371 type: Twig.logic.type.setcapture, 2372 regex: /^set\s+([a-zA-Z0-9_,\s]+)$/, 2373 next: [ 2374 Twig.logic.type.endset 2375 ], 2376 open: true, 2377 compile: function (token) { 2378 var key = token.match[1].trim(); 2379 2380 token.key = key; 2381 2382 delete token.match; 2383 return token; 2384 }, 2385 parse: function (token, context, continue_chain) { 2386 2387 var value = Twig.parse.apply(this, [token.output, context]), 2388 key = token.key; 2389 2390 // set on both the global and local context 2391 this.context[key] = value; 2392 context[key] = value; 2393 2394 return { 2395 chain: continue_chain, 2396 context: context 2397 }; 2398 } 2399 }, 2400 { 2401 /** 2402 * End set type block logic tokens. 2403 * 2404 * Format: {% endset %} 2405 */ 2406 type: Twig.logic.type.endset, 2407 regex: /^endset$/, 2408 next: [ ], 2409 open: false 2410 }, 2411 { 2412 /** 2413 * Filter logic tokens. 2414 * 2415 * Format: {% filter upper %} or {% filter lower|escape %} 2416 */ 2417 type: Twig.logic.type.filter, 2418 regex: /^filter\s+(.+)$/, 2419 next: [ 2420 Twig.logic.type.endfilter 2421 ], 2422 open: true, 2423 compile: function (token) { 2424 var expression = "|" + token.match[1].trim(); 2425 // Compile the expression. 2426 token.stack = Twig.expression.compile.apply(this, [{ 2427 type: Twig.expression.type.expression, 2428 value: expression 2429 }]).stack; 2430 delete token.match; 2431 return token; 2432 }, 2433 parse: function (token, context, chain) { 2434 var unfiltered = Twig.parse.apply(this, [token.output, context]), 2435 stack = [{ 2436 type: Twig.expression.type.string, 2437 value: unfiltered 2438 }].concat(token.stack); 2439 2440 var output = Twig.expression.parse.apply(this, [stack, context]); 2441 2442 return { 2443 chain: chain, 2444 output: output 2445 }; 2446 } 2447 }, 2448 { 2449 /** 2450 * End filter logic tokens. 2451 * 2452 * Format: {% endfilter %} 2453 */ 2454 type: Twig.logic.type.endfilter, 2455 regex: /^endfilter$/, 2456 next: [ ], 2457 open: false 2458 }, 2459 { 2460 /** 2461 * Block logic tokens. 2462 * 2463 * Format: {% block title %} 2464 */ 2465 type: Twig.logic.type.block, 2466 regex: /^block\s+([a-zA-Z0-9_]+)$/, 2467 next: [ 2468 Twig.logic.type.endblock 2469 ], 2470 open: true, 2471 compile: function (token) { 2472 token.block = token.match[1].trim(); 2473 delete token.match; 2474 return token; 2475 }, 2476 parse: function (token, context, chain) { 2477 var block_output = "", 2478 output = "", 2479 isImported = this.importedBlocks.indexOf(token.block) > -1, 2480 hasParent = this.blocks[token.block] && this.blocks[token.block].indexOf(Twig.placeholders.parent) > -1; 2481 2482 // Don't override previous blocks unless they're imported with "use" 2483 // Loops should be exempted as well. 2484 if (this.blocks[token.block] === undefined || isImported || hasParent || context.loop || token.overwrite) { 2485 block_output = Twig.expression.parse.apply(this, [{ 2486 type: Twig.expression.type.string, 2487 value: Twig.parse.apply(this, [token.output, context]) 2488 }, context]); 2489 2490 if (isImported) { 2491 // once the block is overridden, remove it from the list of imported blocks 2492 this.importedBlocks.splice(this.importedBlocks.indexOf(token.block), 1); 2493 } 2494 2495 if (hasParent) { 2496 this.blocks[token.block] = Twig.Markup(this.blocks[token.block].replace(Twig.placeholders.parent, block_output)); 2497 } else { 2498 this.blocks[token.block] = block_output; 2499 } 2500 2501 this.originalBlockTokens[token.block] = { 2502 type: token.type, 2503 block: token.block, 2504 output: token.output, 2505 overwrite: true 2506 }; 2507 } 2508 2509 // Check if a child block has been set from a template extending this one. 2510 if (this.child.blocks[token.block]) { 2511 output = this.child.blocks[token.block]; 2512 } else { 2513 output = this.blocks[token.block]; 2514 } 2515 2516 return { 2517 chain: chain, 2518 output: output 2519 }; 2520 } 2521 }, 2522 { 2523 /** 2524 * End block logic tokens. 2525 * 2526 * Format: {% endblock %} 2527 */ 2528 type: Twig.logic.type.endblock, 2529 regex: /^endblock(?:\s+([a-zA-Z0-9_]+))?$/, 2530 next: [ ], 2531 open: false 2532 }, 2533 { 2534 /** 2535 * Block logic tokens. 2536 * 2537 * Format: {% extends "template.twig" %} 2538 */ 2539 type: Twig.logic.type.extends_, 2540 regex: /^extends\s+(.+)$/, 2541 next: [ ], 2542 open: true, 2543 compile: function (token) { 2544 var expression = token.match[1].trim(); 2545 delete token.match; 2546 2547 token.stack = Twig.expression.compile.apply(this, [{ 2548 type: Twig.expression.type.expression, 2549 value: expression 2550 }]).stack; 2551 2552 return token; 2553 }, 2554 parse: function (token, context, chain) { 2555 // Resolve filename 2556 var file = Twig.expression.parse.apply(this, [token.stack, context]); 2557 2558 // Set parent template 2559 this.extend = file; 2560 2561 return { 2562 chain: chain, 2563 output: '' 2564
2564 }; 2565 } 2566 }, 2567 { 2568 /** 2569 * Block logic tokens. 2570 * 2571 * Format: {% use "template.twig" %} 2572 */ 2573 type: Twig.logic.type.use, 2574 regex: /^use\s+(.+)$/, 2575 next: [ ], 2576 open: true, 2577 compile: function (token) { 2578 var expression = token.match[1].trim(); 2579 delete token.match; 2580 2581 token.stack = Twig.expression.compile.apply(this, [{ 2582 type: Twig.expression.type.expression, 2583 value: expression 2584 }]).stack; 2585 2586 return token; 2587 }, 2588 parse: function (token, context, chain) { 2589 // Resolve filename 2590 var file = Twig.expression.parse.apply(this, [token.stack, context]); 2591 2592 // Import blocks 2593 this.importBlocks(file); 2594 2595 return { 2596 chain: chain, 2597 output: '' 2598 }; 2599 } 2600 }, 2601 { 2602 /** 2603 * Block logic tokens. 2604 * 2605 * Format: {% includes "template.twig" [with {some: 'values'} only] %} 2606 */ 2607 type: Twig.logic.type.include, 2608 regex: /^include\s+(ignore missing\s+)?(.+?)\s*(?:with\s+([\S\s]+?))?\s*(only)?$/, 2609 next: [ ], 2610 open: true, 2611 compile: function (token) { 2612 var match = token.match, 2613 includeMissing = match[1] !== undefined, 2614 expression = match[2].trim(), 2615 withContext = match[3], 2616 only = ((match[4] !== undefined) && match[4].length); 2617 2618 delete token.match; 2619 2620 token.only = only; 2621 token.includeMissing = includeMissing; 2622 2623 token.stack = Twig.expression.compile.apply(this, [{ 2624 type: Twig.expression.type.expression, 2625 value: expression 2626 }]).stack; 2627 2628 if (withContext !== undefined) { 2629 token.withStack = Twig.expression.compile.apply(this, [{ 2630 type: Twig.expression.type.expression, 2631 value: withContext.trim() 2632 }]).stack; 2633 } 2634 2635 return token; 2636 }, 2637 parse: function (token, context, chain) { 2638 // Resolve filename 2639 var innerContext = {}, 2640 withContext, 2641 i, 2642 template; 2643 2644 if (!token.only) { 2645 innerContext = Twig.ChildContext(context); 2646 } 2647 2648 if (token.withStack !== undefined) { 2649 withContext = Twig.expression.parse.apply(this, [token.withStack, context]); 2650 2651 for (i in withContext) { 2652 if (withContext.hasOwnProperty(i)) 2653 innerContext[i] = withContext[i]; 2654 } 2655 } 2656 2657 var file = Twig.expression.parse.apply(this, [token.stack, innerContext]); 2658 2659 if (file instanceof Twig.Template) { 2660 template = file; 2661 } else { 2662 // Import file 2663 template = this.importFile(file); 2664 } 2665 2666 return { 2667 chain: chain, 2668 output: template.render(innerContext) 2669 }; 2670 } 2671 }, 2672 { 2673 type: Twig.logic.type.spaceless, 2674 regex: /^spaceless$/, 2675 next: [ 2676 Twig.logic.type.endspaceless 2677 ], 2678 open: true, 2679 2680 // Parse the html and return it without any spaces between tags 2681 parse: function (token, context, chain) { 2682 var // Parse the output without any filter 2683 unfiltered = Twig.parse.apply(this, [token.output, context]), 2684 // A regular expression to find closing and opening tags with spaces between them 2685 rBetweenTagSpaces = />\s+</g, 2686 // Replace all space between closing and opening html tags 2687 output = unfiltered.replace(rBetweenTagSpaces,'><').trim(); 2688 2689 return { 2690 chain: chain, 2691 output: output 2692 }; 2693 } 2694 }, 2695 2696 // Add the {% endspaceless %} token 2697 { 2698 type: Twig.logic.type.endspaceless, 2699 regex: /^endspaceless$/, 2700 next: [ ], 2701 open: false 2702 }, 2703 { 2704 /** 2705 * Macro logic tokens. 2706 * 2707 * Format: {% maro input(name, value, type, size) %} 2708 * 2709 */ 2710 type: Twig.logic.type.macro, 2711 regex: /^macro\s+([a-zA-Z0-9_]+)\s*\(\s*((?:[a-zA-Z0-9_]+(?:,\s*)?)*)\s*\)$/, 2712 next: [ 2713 Twig.logic.type.endmacro 2714 ], 2715 open: true, 2716 compile: function (token) { 2717 var macroName = token.match[1], 2718 parameters = token.match[2].split(/[\s,]+/); 2719 2720 //TODO: Clean up duplicate check 2721 for (var i=0; i<parameters.length; i++) { 2722 for (var j=0; j<parameters.length; j++){ 2723 if (parameters[i] === parameters[j] && i !== j) {
2724 throw new Twig.Error("Duplicate arguments for parameter: "+ parameters[i]); 2725 } 2726 } 2727 } 2728 2729 token.macroName = macroName; 2730 token.parameters = parameters; 2731 2732 delete token.match; 2733 return token; 2734 }, 2735 parse: function (token, context, chain) { 2736 var template = this; 2737 this.macros[token.macroName] = function() { 2738 // Pass global context and other macros 2739 var macroContext = { 2740 _self: template.macros 2741 } 2742 // Add parameters from context to macroContext 2743 for (var i=0; i<token.parameters.length; i++) { 2744 var prop = token.parameters[i]; 2745 if(typeof arguments[i] !== 'undefined') { 2746 macroContext[prop] = arguments[i]; 2747 } else { 2748 macroContext[prop] = undefined; 2749 } 2750 } 2751 // Render 2752 return Twig.parse.apply(template, [token.output, macroContext]) 2753 }; 2754 2755 return { 2756 chain: chain, 2757 output: '' 2758 }; 2759 2760 } 2761 }, 2762 { 2763 /** 2764 * End macro logic tokens. 2765 * 2766 * Format: {% endmacro %} 2767 */ 2768 type: Twig.logic.type.endmacro, 2769 regex: /^endmacro$/, 2770 next: [ ], 2771 open: false 2772 }, 2773 { 2774 /* 2775 * import logic tokens. 2776 * 2777 * Format: {% import "template.twig" as form %} 2778 */ 2779 type: Twig.logic.type.import_, 2780 regex: /^import\s+(.+)\s+as\s+([a-zA-Z0-9_]+)$/, 2781 next: [ ], 2782 open: true, 2783 compile: function (token) { 2784 var expression = token.match[1].trim(), 2785 contextName = token.match[2].trim(); 2786 delete token.match; 2787 2788 token.expression = expression; 2789 token.contextName = contextName; 2790 2791 token.stack = Twig.expression.compile.apply(this, [{ 2792 type: Twig.expression.type.expression, 2793 value: expression 2794 }]).stack; 2795 2796 return token; 2797 }, 2798 parse: function (token, context, chain) { 2799 if (token.expression !== "_self") { 2800 var file = Twig.expression.parse.apply(this, [token.stack, context]); 2801 var template = this.importFile(file || token.expression); 2802 context[token.contextName] = template.render({}, {output: 'macros'}); 2803 } 2804 else { 2805 context[token.contextName] = this.macros; 2806 } 2807 2808 return { 2809 chain: chain, 2810 output: '' 2811 } 2812 2813 } 2814 }, 2815 { 2816 /* 2817 * from logic tokens. 2818 * 2819 * Format: {% from "template.twig" import func as form %} 2820 */ 2821 type: Twig.logic.type.from, 2822 regex: /^from\s+(.+)\s+import\s+([a-zA-Z0-9_, ]+)$/, 2823 next: [ ], 2824 open: true, 2825 compile: function (token) { 2826 var expression = token.match[1].trim(), 2827 macroExpressions = token.match[2].trim().split(/[ ,]+/), 2828 macroNames = {}; 2829 2830 for (var i=0; i<macroExpressions.length; i++) { 2831 var res = macroExpressions[i]; 2832 2833 // match function as variable 2834 var macroMatch = res.match(/^([a-zA-Z0-9_]+)\s+(.+)\s+as\s+([a-zA-Z0-9_]+)$/); 2835 if (macroMatch) { 2836 macroNames[macroMatch[1].trim()] = macroMatch[2].trim(); 2837 } 2838 else if (res.match(/^([a-zA-Z0-9_]+)$/)) { 2839 macroNames[res] = res; 2840 } 2841 else { 2842 // ignore import 2843 } 2844 2845 } 2846 2847 delete token.match; 2848 2849 token.expression = expression; 2850 token.macroNames = macroNames; 2851 2852 token.stack = Twig.expression.compile.apply(this, [{ 2853 type: Twig.expression.type.expression, 2854 value: expression 2855 }]).stack; 2856 2857 return token; 2858 }, 2859 parse: function (token, context, chain) { 2860 var macros; 2861 2862 if (token.expression !== "_self") { 2863 var file = Twig.expression.parse.apply(this, [token.stack, context]); 2864 var template = this.importFile(file || token.expression); 2865 macros = template.render({}, {output: 'macros'}); 2866 } 2867 else { 2868 macros = this.macros; 2869 } 2870 2871 for (var macroName in token.macroNames) { 2872 if (macros.hasOwnProperty(macroName)) { 2873 context[token.macroNames[macroName]] = macros[macroName]; 2874 } 2875 } 2876 2877 return { 2878 chain: chain, 2879 output: '' 2880 } 2881 2882 } 2883 }, 2884 { 2885 /** 2886 * The embed tag combines the behaviour of include and extends. 2887 * It allows you to include another template's contents, just like include does. 2888 * 2889 * Format: {% embed "template.twig" [with {some: 'values'} only] %} 2890 */ 2891 type: Twig.logic.type.embed, 2892 regex: /^embed\s+(ignore missing\s+)?(.+?)\s*(?:with\s+(.+?))?\s*(only)?$/, 2893 next: [ 2894 Twig.logic.type.endembed 2895 ], 2896 open: true, 2897 compile: function (token) { 2898 var match = token.match, 2899 includeMissing = match[1] !== undefined, 2900 expression = match[2].trim(), 2901 withContext = match[3], 2902 only = ((match[4] !== undefined) && match[4].length); 2903 2904 delete token.match; 2905 2906 token.only = only; 2907 token.includeMissing = includeMissing; 2908 2909 token.stack = Twig.expression.compile.apply(this, [{ 2910 type: Twig.expression.type.expression, 2911 value: expression 2912 }]).stack; 2913 2914 if (withContext !== undefined) { 2915 token.withStack = Twig.expression.compile.apply(this, [{ 2916 type: Twig.expression.type.expression, 2917 value: withContext.trim() 2918 }]).stack; 2919 } 2920 2921 return token; 2922 }, 2923 parse: function (token, context, chain) { 2924 // Resolve filename 2925 var innerContext = {}, 2926 withContext, 2927 i, 2928 template; 2929 2930 if (!token.only) { 2931 for (i in context) { 2932 if (context.hasOwnProperty(i)) 2933 innerContext[i] = context[i]; 2934 } 2935 } 2936 2937 if (token.withStack !== undefined) { 2938 withContext = Twig.expression.parse.apply(this, [token.withStack, context]); 2939 2940 for (i in withContext) { 2941 if (withContext.hasOwnProperty(i)) 2942 innerContext[i] = withContext[i]; 2943 } 2944 } 2945 2946 var file = Twig.expression.parse.apply(this, [token.stack, innerContext]); 2947 2948 if (file instanceof Twig.Template) { 2949 template = file; 2950 } else { 2951 // Import file 2952 template = this.importFile(file); 2953 } 2954 2955 // reset previous blocks 2956 this.blocks = {}; 2957 2958 // parse tokens. output will be not used 2959 var output = Twig.parse.apply(this, [token.output, innerContext]); 2960 2961 // render tempalte with blocks defined in embed block 2962 return { 2963 chain: chain, 2964 output: template.render(innerContext, {'blocks':this.blocks}
2964) 2965 }; 2966 } 2967 }, 2968 /* Add the {% endembed %} token 2969 * 2970 */ 2971 { 2972 type: Twig.logic.type.endembed, 2973 regex: /^endembed$/, 2974 next: [ ], 2975 open: false 2976 } 2977 2978 ]; 2979 2980 2981 /** 2982 * Registry for logic handlers. 2983 */ 2984 Twig.logic.handler = {}; 2985 2986 /** 2987 * Define a new token type, available at Twig.logic.type.{type} 2988 */ 2989 Twig.logic.extendType = function (type, value) { 2990 value = value || ("Twig.logic.type" + type); 2991 Twig.logic.type[type] = value; 2992 }; 2993 2994 /** 2995 * Extend the logic parsing functionality with a new token definition. 2996 * 2997 * // Define a new tag 2998 * Twig.logic.extend({ 2999 * type: Twig.logic.type.{type}, 3000 * // The pattern to match for this token 3001 * regex: ..., 3002 * // What token types can follow this token, leave blank if any. 3003 * next: [ ... ] 3004 * // Create and return compiled version of the token 3005 * compile: function(token) { ... } 3006 * // Parse the compiled token with the context provided by the render call 3007 * // and whether this token chain is complete. 3008 * parse: function(token, context, chain) { ... } 3009 * }); 3010 * 3011 * @param {Object} definition The new logic expression. 3012 */ 3013 Twig.logic.extend = function (definition) { 3014 3015 if (!definition.type) { 3016 throw new Twig.Error("Unable to extend logic definition. No type provided for " + definition); 3017 } else { 3018 Twig.logic.extendType(definition.type); 3019 } 3020 Twig.logic.handler[definition.type] = definition; 3021 }; 3022 3023 // Extend with built-in expressions 3024 while (Twig.logic.definitions.length > 0) { 3025 Twig.logic.extend(Twig.logic.definitions.shift()); 3026 } 3027 3028 /** 3029 * Compile a logic token into an object ready for parsing. 3030 * 3031 * @param {Object} raw_token An uncompiled logic token. 3032 * 3033 * @return {Object} A compiled logic token, ready for parsing. 3034 */ 3035 Twig.logic.compile = function (raw_token) { 3036 var expression = raw_token.value.trim(), 3037 token = Twig.logic.tokenize.apply(this, [expression]), 3038 token_template = Twig.logic.handler[token.type]; 3039 3040 // Check if the token needs compiling 3041 if (token_template.compile) { 3042 token = token_template.compile.apply(this, [token]); 3043 Twig.log.trace("Twig.logic.compile: ", "Compiled logic token to ", token); 3044 } 3045 3046 return token; 3047 }; 3048 3049 /** 3050 * Tokenize logic expressions. This function matches token expressions against regular 3051 * expressions provided in token definitions provided with Twig.logic.extend. 3052 * 3053 * @param {string} expression the logic token expression to tokenize 3054 * (i.e. what's between {% and %}) 3055 * 3056 * @return {Object} The matched token with type set to the token type and match to the regex match. 3057 */ 3058 Twig.logic.tokenize = function (expression) { 3059 var token = {}, 3060 token_template_type = null, 3061 token_type = null, 3062 token_regex = null, 3063 regex_array = null, 3064 regex = null, 3065 match = null; 3066 3067 // Ignore whitespace around expressions. 3068 expression = expression.trim(); 3069 3070 for (token_template_type in Twig.logic.handler) { 3071 if (Twig.logic.handler.hasOwnProperty(token_template_type)) { 3072 // Get the type and regex for this template type 3073 token_type = Twig.logic.handler[token_template_type].type; 3074 token_regex = Twig.logic.handler[token_template_type].regex; 3075 3076 // Handle multiple regular expressions per type. 3077 regex_array = []; 3078 if (token_regex instanceof Array) { 3079 regex_array = token_regex; 3080 } else { 3081 regex_array.push(token_regex); 3082 } 3083 3084 // Check regular expressions in the order they were specified in the definition. 3085 while (regex_array.length > 0) { 3086 regex = regex_array.shift(); 3087 match = regex.exec(expression.trim()); 3088 if (match !== null) { 3089 token.type = token_type; 3090 token.match = match; 3091 Twig.log.trace("Twig.logic.tokenize: ", "Matched a ", token_type, " regular expression of ", match); 3092 return token; 3093 } 3094 } 3095 } 3096 } 3097 3098 // No regex matches
3099 throw new Twig.Error("Unable to parse '" + expression.trim() + "'"); 3100 }; 3101 3102 /** 3103 * Parse a logic token within a given context. 3104 * 3105 * What are logic chains? 3106 * Logic chains represent a series of tokens that are connected, 3107 * for example: 3108 * {% if ... %} {% else %} {% endif %} 3109 * 3110 * The chain parameter is used to signify if a chain is open of closed. 3111 * open: 3112 * More tokens in this chain should be parsed. 3113 * closed: 3114 * This token chain has completed parsing and any additional 3115 * tokens (else, elseif, etc...) should be ignored. 3116 * 3117 * @param {Object} token The compiled token. 3118 * @param {Object} context The render context. 3119 * @param {boolean} chain Is this an open logic chain. If false, that means a 3120 * chain is closed and no further cases should be parsed. 3121 */ 3122 Twig.logic.parse = function (token, context, chain) { 3123 var output = '', 3124 token_template; 3125 3126 context = context || { }; 3127 3128 Twig.log.debug("Twig.logic.parse: ", "Parsing logic token ", token); 3129 3130 token_template = Twig.logic.handler[token.type]; 3131 3132 if (token_template.parse) { 3133 output = token_template.parse.apply(this, [token, context, chain]); 3134 } 3135 return output; 3136 }; 3137 3138 return Twig; 3139 3140})(Twig || { }); 3141// Twig.js 3142// Available under the BSD 2-Clause License 3143// https://github.com/justjohn/twig.js 3144 3145// ## twig.expression.js 3146// 3147// This file handles tokenizing, compiling and parsing expressions. 3148var Twig = (function (Twig) { 3149 "use strict"; 3150 3151 /** 3152 * Namespace for expression handling. 3153 */ 3154 Twig.expression = { }; 3155 3156 /** 3157 * Reserved word that can't be used as variable names. 3158 */ 3159 Twig.expression.reservedWords = [ 3160 "true", "false", "null", "TRUE", "FALSE", "NULL", "_context" 3161 ]; 3162 3163 /** 3164 * The type of tokens used in expressions. 3165 */ 3166 Twig.expression.type = { 3167 comma: 'Twig.expression.type.comma', 3168 operator: { 3169 unary: 'Twig.expression.type.operator.unary', 3170 binary: 'Twig.expression.type.operator.binary' 3171 }, 3172 string: 'Twig.expression.type.string', 3173 bool: 'Twig.expression.type.bool', 3174 array: { 3175 start: 'Twig.expression.type.array.start', 3176 end: 'Twig.expression.type.array.end' 3177 }, 3178 object: { 3179 start: 'Twig.expression.type.object.start', 3180 end: 'Twig.expression.type.object.end' 3181 }, 3182 parameter: { 3183 start: 'Twig.expression.type.parameter.start', 3184 end: 'Twig.expression.type.parameter.end' 3185 }, 3186 key: { 3187 period: 'Twig.expression.type.key.period', 3188 brackets: 'Twig.expression.type.key.brackets' 3189 }, 3190 filter: 'Twig.expression.type.filter', 3191 _function: 'Twig.expression.type._function', 3192 variable: 'Twig.expression.type.variable', 3193 number: 'Twig.expression.type.number', 3194 _null: 'Twig.expression.type.null', 3195 context: 'Twig.expression.type.context', 3196 test: 'Twig.expression.type.test' 3197 }; 3198 3199 Twig.expression.set = { 3200 // What can follow an expression (in general) 3201 operations: [ 3202 Twig.expression.type.filter, 3203 Twig.expression.type.operator.unary, 3204 Twig.expression.type.operator.binary, 3205 Twig.expression.type.array.end, 3206 Twig.expression.type.object.end, 3207 Twig.expression.type.parameter.end, 3208 Twig.expression.type.comma, 3209 Twig.expression.type.test 3210 ], 3211 expressions: [ 3212 Twig.expression.type._function, 3213 Twig.expression.type.bool, 3214 Twig.expression.type.string, 3215 Twig.expression.type.variable, 3216 Twig.expression.type.number, 3217 Twig.expression.type._null, 3218 Twig.expression.type.context, 3219 Twig.expression.type.parameter.start, 3220 Twig.expression.type.array.start, 3221 Twig.expression.type.object.start 3222 ] 3223 }; 3224 3225 // Most expressions allow a '.' or '[' after them, so we provide a convenience set 3226 Twig.expression.set.operations_extended = Twig.expression.set.operations.concat([ 3227 Twig.expression.type.key.period, 3228 Twig.expression.type.key.brackets]); 3229 3230 // Some commonly used compile and parse functions. 3231 Twig.expression.fn = { 3232 compile: { 3233 push: function(token, stack, output) { 3234 output.push(token); 3235 }, 3236 push_both: function(token, stack, output) { 3237 output.push(token); 3238 stack.push(token); 3239 } 3240 }, 3241 parse: { 3242 push: function(token, stack, context) { 3243 stack.push(token); 3244 }, 3245 push_value: function(token, stack, context) { 3246 stack.push(token.value); 3247 } 3248 } 3249 }; 3250 3251 // The regular expressions and compile/parse logic used to match tokens in expressions. 3252 // 3253 // Properties: 3254 // 3255 // type: The type of expression this matches 3256 // 3257 // regex: One or more regular expressions that matche the format of the token. 3258 // 3259 // next: Valid tokens that can occur next in the expression. 3260 // 3261 // Functions: 3262 // 3263 // compile: A function that compiles the raw regular expression match into a token. 3264 // 3265 // parse: A function that parses the compiled token into output. 3266 // 3267 Twig.expression.definitions = [ 3268 { 3269 type: Twig.expression.type.test, 3270 regex: /^is\s+(not)?\s*([a-zA-Z_][a-zA-Z0-9_]*)/, 3271 next: Twig.expression.set.operations.concat([Twig.expression.type.parameter.start]), 3272 compile: function(token, stack, output) { 3273 token.filter = token.match[2]; 3274 token.modifier = token.match[1]; 3275 delete token.match; 3276 delete token.value; 3277 output.push(token); 3278 }, 3279 parse: function(token, stack, context) { 3280 var value = stack.pop(), 3281 params = token.params && Twig.expression.parse.apply(this, [token.params, context]), 3282 result = Twig.test(token.filter, value, params); 3283 3284 if (token.modifier == 'not') { 3285 stack.push(!result); 3286 } else { 3287 stack.push(result); 3288 } 3289 } 3290 }, 3291 { 3292 type: Twig.expression.type.comma, 3293 // Match a comma 3294 regex: /^,/, 3295 next: Twig.expression.set.expressions.concat([Twig.expression.type.array.end, Twig.expression.type.object.end]), 3296 compile: function(token, stack, output) { 3297 var i = stack.length - 1, 3298 stack_token; 3299 3300 delete token.match; 3301 delete token.value; 3302 3303 // pop tokens off the stack until the start of the object 3304 for(;i >= 0; i--) { 3305 stack_token = stack.pop(); 3306 if (stack_token.type === Twig.expression.type.object.start 3307 || stack_token.type === Twig.expression.type.parameter.start 3308 || stack_token.type === Twig.expression.type.array.start) { 3309 stack.push(stack_token); 3310 break; 3311 } 3312 output.push(stack_token); 3313 } 3314 output.push(token); 3315 } 3316 }, 3317 { 3318 type: Twig.expression.type.operator.binary, 3319 // Match any of +, *, /, -, %, ~, <, <=, >, >=, !=, ==, **, ?, :, and, or, not 3320 regex: /(^[\+\-~%\?\:]|^[!=]==?|^[!<>]=?|^\*\*?|^\/\/?|^and\s+|^or\s+|^in\s+|^not in\s+|^\.\.)/, 3321 next: Twig.expression.set.expressions.concat([Twig.expression.type.operator.unary]), 3322 compile: function(token, stack, output) { 3323 delete token.match; 3324 3325 token.value = token.value.trim(); 3326 var value = token.value, 3327 operator = Twig.expression.operator.lookup(value, token); 3328 3329 Twig.log.trace("Twig.expression.compile: ", "Operator: ", operator, " from ", value); 3330 3331 while (stack.length > 0 && 3332 (stack[stack.length-1].type == Twig.expression.type.operator.unary || stack[stack.length-1].type == Twig.expression.type.operator.binary) && 3333 ( 3334 (operator.associativity === Twig.expression.operator.leftToRight && 3335 operator.precidence >= stack[stack.length-1].precidence) || 3336 3337 (operator.associativity === Twig.expression.operator.rightToLeft && 3338 operator.precidence > stack[stack.length-1].precidence) 3339 ) 3340 ) { 3341 var temp = stack.pop(); 3342 output.push(temp); 3343 } 3344 3345 if (value === ":") { 3346 // Check if this is a ternary or object key being set 3347 if (stack[stack.length - 1] && stack[stack.length-1].value === "?") { 3348 // Continue as normal for a ternary 3349 } else { 3350 // This is not a ternary so we push the token to the output where it can be handled 3351 // when the assocated object is closed. 3352 var key_token = output.pop(); 3353 3354 if (key_token.type === Twig.expression.type.string || 3355 key_token.type === Twig.expression.type.variable || 3356 key_token.type === Twig.expression.type.number) { 3357 token.key = key_token.value; 3358 3359 } else {
3360 throw new Twig.Error("Unexpected value before ':' of " + key_token.type + " = " + key_token.value); 3361 } 3362 3363 output.push(token); 3364 return; 3365 } 3366 } else { 3367 stack.push(operator); 3368 } 3369 }, 3370 parse: function(token, stack, context) { 3371 if (token.key) { 3372 // handle ternary ':' operator 3373 stack.push(token); 3374 } else { 3375 Twig.expression.operator.parse(token.value, stack); 3376 } 3377 } 3378 }, 3379 { 3380 type: Twig.expression.type.operator.unary, 3381 // Match any of not 3382 regex: /(^not\s+)/, 3383 next: Twig.expression.set.expressions, 3384 compile: function(token, stack, output) { 3385 delete token.match; 3386 3387 token.value = token.value.trim(); 3388 var value = token.value, 3389 operator = Twig.expression.operator.lookup(value, token); 3390 3391 Twig.log.trace("Twig.expression.compile: ", "Operator: ", operator, " from ", value); 3392 3393 while (stack.length > 0 && 3394 (stack[stack.length-1].type == Twig.expression.type.operator.unary || stack[stack.length-1].type == Twig.expression.type.operator.binary) && 3395 ( 3396 (operator.associativity === Twig.expression.operator.leftToRight && 3397 operator.precidence >= stack[stack.length-1].precidence) || 3398 3399 (operator.associativity === Twig.expression.operator.rightToLeft && 3400 operator.precidence > stack[stack.length-1].precidence) 3401 ) 3402 ) { 3403 var temp = stack.pop(); 3404 output.push(temp); 3405 } 3406 3407 stack.push(operator); 3408 }, 3409 parse: function(token, stack, context) { 3410 Twig.expression.operator.parse(token.value, stack); 3411 } 3412 }, 3413 { 3414 /** 3415 * Match a string. This is anything between a pair of single or double quotes. 3416 */ 3417 type: Twig.expression.type.string, 3418 // See: http://blog.stevenlevithan.com/archives/match-quoted-string 3419 regex: /^(["'])(?:(?=(\\?))\2[\s\S])*?\1/, 3420 next: Twig.expression.set.operations, 3421 compile: function(token, stack, output) { 3422 var value = token.value; 3423 delete token.match 3424 3425 // Remove the quotes from the string 3426 if (value.substring(0, 1) === '"') { 3427 value = value.replace('\\"', '"'); 3428 } else { 3429 value = value.replace("\\'", "'"); 3430 } 3431 token.value = value.substring(1, value.length-1).replace( /\\n/g, "\n" ).replace( /\\r/g, "\r" ); 3432 Twig.log.trace("Twig.expression.compile: ", "String value: ", token.value); 3433 output.push(token); 3434 }, 3435 parse: Twig.expression.fn.parse.push_value 3436 }, 3437 { 3438 /** 3439 * Match a parameter set start. 3440 */ 3441 type: Twig.expression.type.parameter.start, 3442 regex: /^\(/, 3443 next: Twig.expression.set.expressions.concat([Twig.expression.type.parameter.end]), 3444 compile: Twig.expression.fn.compile.push_both, 3445 parse: Twig.expression.fn.parse.push 3446 }, 3447 { 3448 /** 3449 * Match a parameter set end. 3450 */ 3451 type: Twig.expression.type.parameter.end, 3452 regex: /^\)/, 3453 next: Twig.expression.set.operations_extended, 3454 compile: function(token, stack, output) { 3455 var stack_token, 3456 end_token = token; 3457 3458 stack_token = stack.pop(); 3459 while(stack.length > 0 && stack_token.type != Twig.expression.type.parameter.start) { 3460 output.push(stack_token); 3461 stack_token = stack.pop(); 3462 } 3463 3464 // Move contents of parens into preceding filter 3465 var param_stack = []; 3466 while(token.type !== Twig.expression.type.parameter.start) { 3467 // Add token to arguments stack 3468 param_stack.unshift(token); 3469 token = output.pop(); 3470 } 3471 param_stack.unshift(token); 3472 3473 var is_expression = false;
3474 3475 // Get the token preceding the parameters 3476 token = output[output.length-1]; 3477 3478 if (token === undefined || 3479 (token.type !== Twig.expression.type._function && 3480 token.type !== Twig.expression.type.filter && 3481 token.type !== Twig.expression.type.test && 3482 token.type !== Twig.expression.type.key.brackets && 3483 token.type !== Twig.expression.type.key.period)) { 3484 3485 end_token.expression = true; 3486 3487 // remove start and end token from stack 3488 param_stack.pop(); 3489 param_stack.shift(); 3490 3491 end_token.params = param_stack; 3492 3493 output.push(end_token); 3494 3495 } else { 3496 end_token.expression = false; 3497 token.params = param_stack; 3498 } 3499 }, 3500 parse: function(token, stack, context) { 3501 var new_array = [], 3502 array_ended = false, 3503 value = null; 3504 3505 if (token.expression) { 3506 value = Twig.expression.parse.apply(this, [token.params, context]) 3507 stack.push(value); 3508 3509 } else { 3510 3511 while (stack.length > 0) { 3512 value = stack.pop(); 3513 // Push values into the array until the start of the array 3514 if (value && value.type && value.type == Twig.expression.type.parameter.start) { 3515 array_ended = true; 3516 break; 3517 } 3518 new_array.unshift(value); 3519 } 3520 3521 if (!array_ended) { 3522 throw new Twig.Error("Expected end of parameter set."); 3523 } 3524 3525 stack.push(new_array); 3526 } 3527 } 3528 }, 3529 { 3530 /** 3531 * Match an array start. 3532 */ 3533 type: Twig.expression.type.array.start, 3534 regex: /^\[/, 3535 next: Twig.expression.set.expressions.concat([Twig.expression.type.array.end]), 3536 compile: Twig.expression.fn.compile.push_both, 3537 parse: Twig.expression.fn.parse.push 3538 }, 3539 { 3540 /** 3541 * Match an array end. 3542 */ 3543 type: Twig.expression.type.array.end, 3544 regex: /^\]/, 3545 next: Twig.expression.set.operations_extended, 3546 compile: function(token, stack, output) { 3547 var i = stack.length - 1, 3548 stack_token; 3549 // pop tokens off the stack until the start of the object 3550 for(;i >= 0; i--) { 3551 stack_token = stack.pop(); 3552 if (stack_token.type === Twig.expression.type.array.start) { 3553 break; 3554 } 3555 output.push(stack_token); 3556 } 3557 output.push(token); 3558 }, 3559 parse: function(token, stack, context) { 3560 var new_array = [], 3561 array_ended = false, 3562 value = null; 3563 3564 while (stack.length > 0) { 3565 value = stack.pop(); 3566 // Push values into the array until the start of the array 3567 if (value.type && value.type == Twig.expression.type.array.start) { 3568 array_ended = true; 3569 break; 3570 } 3571 new_array.unshift(value); 3572 } 3573 if (!array_ended) { 3574 throw new Twig.Error("Expected end of array."); 3575 } 3576 3577 stack.push(new_array); 3578 } 3579 }, 3580 // Token that represents the start of a hash map '}' 3581 // 3582 // Hash maps take the form: 3583 // { "key": 'value', "another_key": item } 3584 // 3585 // Keys must be quoted (either single or double) and values can be any expression. 3586 { 3587 type: Twig.expression.type.object.start, 3588 regex: /^\{/, 3589 next: Twig.expression.set.expressions.concat([Twig.expression.type.object.end]), 3590 compile: Twig.expression.fn.compile.push_both, 3591 parse: Twig.expression.fn.parse.push 3592 }, 3593 3594 // Token that represents the end of a Hash Map '}' 3595 // 3596 // This is where the logic for building the internal 3597 // representation of a hash map is defined. 3598 { 3599 type: Twig.expression.type.object.end, 3600 regex: /^\}/, 3601 next: Twig.expression.set.operations_extended, 3602 compile: function(token, stack, output) { 3603 var i = stack.length-1, 3604 stack_token; 3605 3606 // pop tokens off the stack until the start of the object 3607 for(;i >= 0; i--) { 3608 stack_token = stack.pop(); 3609 if (stack_token && stack_token.type === Twig.expression.type.object.start) { 3610 break; 3611 } 3612 output.push(stack_token); 3613 } 3614 output.push(token); 3615 },
3616 parse: function(end_token, stack, context) { 3617 var new_object = {}, 3618 object_ended = false, 3619 token = null, 3620 token_key = null, 3621 has_value = false, 3622 value = null; 3623 3624 while (stack.length > 0) { 3625 token = stack.pop(); 3626 // Push values into the array until the start of the object 3627 if (token && token.type && token.type === Twig.expression.type.object.start) { 3628 object_ended = true; 3629 break; 3630 } 3631 if (token && token.type && (token.type === Twig.expression.type.operator.binary || token.type === Twig.expression.type.operator.unary) && token.key) { 3632 if (!has_value) { 3633 throw new Twig.Error("Missing value for key '" + token.key + "' in object definition."); 3634 } 3635 new_object[token.key] = value; 3636 3637 // Preserve the order that elements are added to the map 3638 // This is necessary since JavaScript objects don't 3639 // guarantee the order of keys 3640 if (new_object._keys === undefined) new_object._keys = []; 3641 new_object._keys.unshift(token.key); 3642 3643 // reset value check 3644 value = null; 3645 has_value = false; 3646 3647 } else { 3648 has_value = true; 3649 value = token; 3650 } 3651 } 3652 if (!object_ended) { 3653 throw new Twig.Error("Unexpected end of object."); 3654 } 3655 3656 stack.push(new_object); 3657 } 3658 }, 3659 3660 // Token representing a filter 3661 // 3662 // Filters can follow any expression and take the form: 3663 // expression|filter(optional, args) 3664 // 3665 // Filter parsing is done in the Twig.filters namespace. 3666 { 3667 type: Twig.expression.type.filter, 3668 // match a | then a letter or _, then any number of letters, numbers, _ or - 3669 regex: /^\|\s?([a-zA-Z_][a-zA-Z0-9_\-]*)/, 3670 next: Twig.expression.set.operations_extended.concat([ 3671 Twig.expression.type.parameter.start]), 3672 compile: function(token, stack, output) { 3673 token.value = token.match[1]; 3674 output.push(token); 3675 }, 3676 parse: function(token, stack, context) { 3677 var input = stack.pop(), 3678 params = token.params && Twig.expression.parse.apply(this, [token.params, context]); 3679 3680 stack.push(Twig.filter.apply(this, [token.value, input, params])); 3681 } 3682 }, 3683 { 3684 type: Twig.expression.type._function, 3685 // match any letter or _, then any number of letters, numbers, _ or - followed by ( 3686 regex: /^([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/, 3687 next: Twig.expression.type.parameter.start, 3688 transform: function(match, tokens) { 3689 return '('; 3690 }, 3691 compile: function(token, stack, output) { 3692 var fn = token.match[1]; 3693 token.fn = fn; 3694 // cleanup token 3695 delete token.match; 3696 delete token.value; 3697 3698 output.push(token); 3699 }, 3700 parse: function(token, stack, context) { 3701 var params = token.params && Twig.expression.parse.apply(this, [token.params, context]), 3702 fn = token.fn, 3703 value; 3704 3705 if (Twig.functions[fn]) { 3706 // Get the function from the built-in functions 3707 value = Twig.functions[fn].apply(this, params); 3708 3709 } else if (typeof context[fn] == 'function') { 3710 // Get the function from the user/context defined functions 3711 value = context[fn].apply(context, params); 3712 3713 } else {
3714 throw new Twig.Error(fn + ' function does not exist and is not defined in the context'); 3715 } 3716 3717 stack.push(value); 3718 } 3719 }, 3720 3721 // Token representing a variable. 3722 // 3723 // Variables can contain letters, numbers, underscores and 3724 // dashes, but must start with a letter or underscore. 3725 // 3726 // Variables are retrieved from the render context and take 3727 // the value of 'undefined' if the given variable doesn't 3728 // exist in the context. 3729 { 3730 type: Twig.expression.type.variable, 3731 // match any letter or _, then any number of letters, numbers, _ or - 3732 regex: /^[a-zA-Z_][a-zA-Z0-9_]*/, 3733 next: Twig.expression.set.operations_extended.concat([ 3734 Twig.expression.type.parameter.start]), 3735 compile: Twig.expression.fn.compile.push, 3736 validate: function(match, tokens) { 3737 return (Twig.indexOf(Twig.expression.reservedWords, match[0]) < 0); 3738 }, 3739 parse: function(token, stack, context) { 3740 // Get the variable from the context 3741 var value = Twig.expression.resolve(context[token.value], context); 3742 stack.push(value); 3743 } 3744 }, 3745 { 3746 type: Twig.expression.type.key.period, 3747 regex: /^\.([a-zA-Z0-9_]+)/, 3748 next: Twig.expression.set.operations_extended.concat([ 3749 Twig.expression.type.parameter.start]), 3750 compile: function(token, stack, output) { 3751 token.key = token.match[1]; 3752 delete token.match; 3753 delete token.value; 3754 3755 output.push(token); 3756 }, 3757 parse: function(token, stack, context) { 3758 var params = token.params && Twig.expression.parse.apply(this, [token.params, context]), 3759 key = token.key, 3760 object = stack.pop(), 3761 value; 3762 3763 if (object === null || object === undefined) { 3764 if (this.options.strict_variables) { 3765 throw new Twig.Error("Can't access a key " + key + " on an null or undefined object."); 3766 } else { 3767 return null; 3768 } 3769 } 3770 3771 var capitalize = function(value) {return value.substr(0, 1).toUpperCase() + value.substr(1);}; 3772 3773 // Get the variable from the context 3774 if (typeof object === 'object' && key in object) { 3775 value = object[key]; 3776 } else if (object["get"+capitalize(key)] !== undefined) { 3777 value = object["get"+capitalize(key)]; 3778 } else if (object["is"+capitalize(key)] !== undefined) { 3779 value = object["is"+capitalize(key)]; 3780 } else { 3781 value = null; 3782 } 3783 stack.push(Twig.expression.resolve(value, object, params)); 3784 } 3785 }, 3786 { 3787 type: Twig.expression.type.key.brackets, 3788 regex: /^\[([^\]]*)\]/, 3789 next: Twig.expression.set.operations_extended.concat([ 3790 Twig.expression.type.parameter.start]), 3791 compile: function(token, stack, output) { 3792 var match = token.match[1]; 3793 delete token.value; 3794 delete token.match; 3795 3796 // The expression stack for the key 3797 token.stack = Twig.expression.compile({ 3798 value: match 3799 }).stack; 3800 3801 output.push(token); 3802 }, 3803 parse: function(token, stack, context) { 3804 // Evaluate key 3805 var params = token.params && Twig.expression.parse.apply(this, [token.params, context]), 3806 key = Twig.expression.parse.apply(this, [token.stack, context]), 3807 object = stack.pop(), 3808 value; 3809 3810 if (object === null || object === undefined) { 3811 if (this.options.strict_variables) {
3812 throw new Twig.Error("Can't access a key " + key + " on an null or undefined object."); 3813 } else { 3814 return null; 3815 } 3816 } 3817 3818 // Get the variable from the context 3819 if (typeof object === 'object' && key in object) { 3820 value = object[key]; 3821 } else { 3822 value = null; 3823 } 3824 stack.push(Twig.expression.resolve(value, object, params)); 3825 } 3826 }, 3827 { 3828 /** 3829 * Match a null value. 3830 */ 3831 type: Twig.expression.type._null, 3832 // match a number 3833 regex: /^(null|NULL|none|NONE)/, 3834 next: Twig.expression.set.operations, 3835 compile: function(token, stack, output) { 3836 delete token.match; 3837 token.value = null; 3838 output.push(token); 3839 }, 3840 parse: Twig.expression.fn.parse.push_value 3841 }, 3842 { 3843 /** 3844 * Match the context 3845 */ 3846 type: Twig.expression.type.context, 3847 regex: /^_context/, 3848 next: Twig.expression.set.operations_extended.concat([ 3849 Twig.expression.type.parameter.start]), 3850 compile: Twig.expression.fn.compile.push, 3851 parse: function(token, stack, context) { 3852 stack.push(context); 3853 } 3854 }, 3855 { 3856 /** 3857 * Match a number (integer or decimal) 3858 */ 3859 type: Twig.expression.type.number, 3860 // match a number 3861 regex: /^\-?\d+(\.\d+)?/, 3862 next: Twig.expression.set.operations, 3863 compile: function(token, stack, output) { 3864 token.value = Number(token.value); 3865 output.push(token); 3866 }, 3867 parse: Twig.expression.fn.parse.push_value 3868 }, 3869 { 3870 /** 3871 * Match a boolean 3872 */ 3873 type: Twig.expression.type.bool, 3874 regex: /^(true|TRUE|false|FALSE)/, 3875 next: Twig.expression.set.operations, 3876 compile: function(token, stack, output) { 3877 token.value = (token.match[0].toLowerCase( ) === "true"); 3878 delete token.match; 3879 output.push(token); 3880 }, 3881 parse: Twig.expression.fn.parse.push_value 3882 } 3883 ]; 3884 3885 /** 3886 * Resolve a context value. 3887 * 3888 * If the value is a function, it is executed with a context parameter. 3889 * 3890 * @param {string} key The context object key. 3891 * @param {Object} context The render context. 3892 */ 3893 Twig.expression.resolve = function(value, context, params) { 3894 if (typeof value == 'function') { 3895 return value.apply(context, params || []); 3896 } else { 3897 return value; 3898 } 3899 }; 3900 3901 /** 3902 * Registry for logic handlers. 3903 */ 3904 Twig.expression.handler = {}; 3905 3906 /** 3907 * Define a new expression type, available at Twig.logic.type.{type} 3908 * 3909 * @param {string} type The name of the new type. 3910 */ 3911 Twig.expression.extendType = function (type) { 3912 Twig.expression.type[type] = "Twig.expression.type." + type; 3913 }; 3914 3915 /** 3916 * Extend the expression parsing functionality with a new definition. 3917 * 3918 * Token definitions follow this format: 3919 * { 3920 * type: One of Twig.expression.type.[type], either pre-defined or added using 3921 * Twig.expression.extendType 3922 * 3923 * next: Array of types from Twig.expression.type that can follow this token, 3924 * 3925 * regex: A regex or array of regex's that should match the token. 3926 * 3927 * compile: function(token, stack, output) called when this token is being compiled. 3928 * Should return an object with stack and output set. 3929 * 3930 * parse: function(token, stack, context) called when this token is being parsed. 3931 * Should return an object with stack and context set. 3932 * } 3933 * 3934 * @param {Object} definition A token definition. 3935 */ 3936 Twig.expression.extend = function (definition) { 3937 if (!definition.type) {
3938 throw new Twig.Error("Unable to extend logic definition. No type provided for " + definition); 3939 } 3940 Twig.expression.handler[definition.type] = definition; 3941 }; 3942 3943 // Extend with built-in expressions 3944 while (Twig.expression.definitions.length > 0) { 3945 Twig.expression.extend(Twig.expression.definitions.shift()); 3946 } 3947 3948 /** 3949 * Break an expression into tokens defined in Twig.expression.definitions. 3950 * 3951 * @param {string} expression The string to tokenize. 3952 * 3953 * @return {Array} An array of tokens. 3954 */ 3955 Twig.expression.tokenize = function (expression) { 3956 var tokens = [], 3957 // Keep an offset of the location in the expression for error messages. 3958 exp_offset = 0, 3959 // The valid next tokens of the previous token 3960 next = null, 3961 // Match information 3962 type, regex, regex_array, 3963 // The possible next token for the match 3964 token_next, 3965 // Has a match been found from the definitions 3966 match_found, invalid_matches = [], match_function; 3967 3968 match_function = function () { 3969 var match = Array.prototype.slice.apply(arguments), 3970 string = match.pop(), 3971 offset = match.pop(); 3972 3973 Twig.log.trace("Twig.expression.tokenize", 3974 "Matched a ", type, " regular expression of ", match); 3975 3976 if (next && Twig.indexOf(next, type) < 0) { 3977 invalid_matches.push( 3978 type + " cannot follow a " + tokens[tokens.length - 1].type + 3979 " at template:" + exp_offset + " near '" + match[0].substring(0, 20) + 3980 "...'" 3981 ); 3982 // Not a match, don't change the expression 3983 return match[0]; 3984 } 3985 3986 // Validate the token if a validation function is provided 3987 if (Twig.expression.handler[type].validate && 3988 !Twig.expression.handler[type].validate(match, tokens)) { 3989 return match[0]; 3990 } 3991 3992 invalid_matches = []; 3993 3994 tokens.push({ 3995 type: type, 3996 value: match[0], 3997 match: match 3998 }); 3999 4000 match_found = true; 4001 next = token_next; 4002 exp_offset += match[0].length; 4003 4004 // Does the token need to return output back to the expression string 4005 // e.g. a function match of cycle( might return the '(' back to the expression 4006 // This allows look-ahead to differentiate between token types (e.g. functions and variable names) 4007 if (Twig.expression.handler[type].transform) { 4008 return Twig.expression.handler[type].transform(match, tokens); 4009 } 4010 return ''; 4011 }; 4012 4013 Twig.log.debug("Twig.expression.tokenize", "Tokenizing expression ", expression); 4014 4015 while (expression.length > 0) { 4016 expression = expression.trim(); 4017 for (type in Twig.expression.handler) { 4018 if (Twig.expression.handler.hasOwnProperty(type)) { 4019 token_next = Twig.expression.handler[type].next; 4020 regex = Twig.expression.handler[type].regex; 4021 // Twig.log.trace("Checking type ", type, " on ", expression); 4022 if (regex instanceof Array) { 4023 regex_array = regex; 4024 } else { 4025 regex_array = [regex]; 4026 } 4027 4028 match_found = false; 4029 while (regex_array.length > 0) { 4030 regex = regex_array.pop(); 4031 expression = expression.replace(regex, match_function); 4032 } 4033 // An expression token has been matched. Break the for loop and start trying to 4034 // match the next template (if expression isn't empty.) 4035 if (match_found) { 4036 break; 4037 } 4038 } 4039 } 4040 if (!match_found) { 4041 if (invalid_matches.length > 0) {
4042 throw new Twig.Error(invalid_matches.join(" OR ")); 4043 } else { 4044 throw new Twig.Error("Unable to parse '" + expression + "' at template position" + exp_offset); 4045 } 4046 } 4047 } 4048 4049 Twig.log.trace("Twig.expression.tokenize", "Tokenized to ", tokens); 4050 return tokens; 4051 }; 4052 4053 /** 4054 * Compile an expression token. 4055 * 4056 * @param {Object} raw_token The uncompiled token. 4057 * 4058 * @return {Object} The compiled token. 4059 */ 4060 Twig.expression.compile = function (raw_token) { 4061 var expression = raw_token.value, 4062 // Tokenize expression 4063 tokens = Twig.expression.tokenize(expression), 4064 token = null, 4065 output = [], 4066 stack = [], 4067 token_template = null; 4068 4069 Twig.log.trace("Twig.expression.compile: ", "Compiling ", expression); 4070 4071 // Push tokens into RPN stack using the Sunting-yard algorithm 4072 // See http://en.wikipedia.org/wiki/Shunting_yard_algorithm 4073 4074 while (tokens.length > 0) { 4075 token = tokens.shift(); 4076 token_template = Twig.expression.handler[token.type]; 4077 4078 Twig.log.trace("Twig.expression.compile: ", "Compiling ", token); 4079 4080 // Compile the template 4081 token_template.compile && token_template.compile(token, stack, output); 4082 4083 Twig.log.trace("Twig.expression.compile: ", "Stack is", stack); 4084 Twig.log.trace("Twig.expression.compile: ", "Output is", output); 4085 } 4086 4087 while(stack.length > 0) { 4088 output.push(stack.pop()); 4089 } 4090 4091 Twig.log.trace("Twig.expression.compile: ", "Final output is", output); 4092 4093 raw_token.stack = output; 4094 delete raw_token.value; 4095 4096 return raw_token; 4097 }; 4098 4099 4100 /** 4101 * Parse an RPN expression stack within a context. 4102 * 4103 * @param {Array} tokens An array of compiled expression tokens. 4104 * @param {Object} context The render context to parse the tokens with. 4105 * 4106 * @return {Object} The result of parsing all the tokens. The result 4107 * can be anything, String, Array, Object, etc... based on 4108 * the given expression. 4109 */ 4110 Twig.expression.parse = function (tokens, context) { 4111 var that = this; 4112 4113 // If the token isn't an array, make it one. 4114 if (!(tokens instanceof Array)) { 4115 tokens = [tokens]; 4116 } 4117 4118 // The output stack 4119 var stack = [], 4120 token_template = null; 4121 4122 Twig.forEach(tokens, function (token) { 4123 token_template = Twig.expression.handler[token.type]; 4124 4125 token_template.parse && token_template.parse.apply(that, [token, stack, context]); 4126 }); 4127 4128 // Pop the final value off the stack 4129 return stack.pop(); 4130 }; 4131 4132 return Twig; 4133 4134})( Twig || { } ); 4135// Twig.js 4136// Available under the BSD 2-Clause License 4137// https://github.com/justjohn/twig.js 4138 4139// ## twig.expression.operator.js 4140// 4141// This file handles operator lookups and parsing. 4142var Twig = (function (Twig) { 4143 "use strict"; 4144 4145 /** 4146 * Operator associativity constants. 4147 */ 4148 Twig.expression.operator = { 4149 leftToRight: 'leftToRight', 4150 rightToLeft: 'rightToLeft' 4151 }; 4152 4153 var containment = function(a, b) { 4154 if (b.indexOf !== undefined) { 4155 // String 4156 return a === b || a !== '' && b.indexOf(a) > -1; 4157 4158 } else { 4159 var el; 4160 for (el in b) { 4161 if (b.hasOwnProperty(el) && b[el] === a) { 4162 return true; 4163 } 4164 } 4165 return false; 4166 } 4167 }; 4168 4169 /** 4170 * Get the precidence and associativity of an operator. These follow the order that C/C++ use. 4171 * See http://en.wikipedia.org/wiki/Operators_in_C_and_C++ for the table of values. 4172 */ 4173 Twig.expression.operator.lookup = function (operator, token) { 4174 switch (operator) { 4175 case "..": 4176 case 'not in': 4177 case 'in': 4178 token.precidence = 20; 4179 token.associativity = Twig.expression.operator.leftToRight; 4180 break; 4181 4182 case ',': 4183 token.precidence = 18;
4184 token.associativity = Twig.expression.operator.leftToRight; 4185 break; 4186 4187 // Ternary 4188 case '?': 4189 case ':': 4190 token.precidence = 16; 4191 token.associativity = Twig.expression.operator.rightToLeft; 4192 break; 4193 4194 case 'or': 4195 token.precidence = 14; 4196 token.associativity = Twig.expression.operator.leftToRight; 4197 break; 4198 4199 case 'and': 4200 token.precidence = 13; 4201 token.associativity = Twig.expression.operator.leftToRight; 4202 break; 4203 4204 case '==': 4205 case '!=': 4206 token.precidence = 9; 4207 token.associativity = Twig.expression.operator.leftToRight; 4208 break; 4209 4210 case '<': 4211 case '<=': 4212 case '>': 4213 case '>=': 4214 token.precidence = 8; 4215 token.associativity = Twig.expression.operator.leftToRight; 4216 break; 4217 4218 4219 case '~': // String concatination 4220 case '+': 4221 case '-': 4222 token.precidence = 6; 4223 token.associativity = Twig.expression.operator.leftToRight; 4224 break; 4225 4226 case '//': 4227 case '**': 4228 case '*': 4229 case '/': 4230 case '%': 4231 token.precidence = 5; 4232 token.associativity = Twig.expression.operator.leftToRight; 4233 break; 4234 4235 case 'not': 4236 token.precidence = 3; 4237 token.associativity = Twig.expression.operator.rightToLeft; 4238 break; 4239 4240 default: 4241 throw new Twig.Error(operator + " is an unknown operator."); 4242 } 4243 token.operator = operator; 4244 return token; 4245 }; 4246 4247 /** 4248 * Handle operations on the RPN stack. 4249 * 4250 * Returns the updated stack. 4251 */ 4252 Twig.expression.operator.parse = function (operator, stack) { 4253 Twig.log.trace("Twig.expression.operator.parse: ", "Handling ", operator); 4254 var a, b, c; 4255 switch (operator) { 4256 case ':': 4257 // Ignore 4258 break; 4259 4260 case '?': 4261 c = stack.pop(); // false expr 4262 b = stack.pop(); // true expr 4263 a = stack.pop(); // conditional 4264 if (a) { 4265 stack.push(b); 4266 } else { 4267 stack.push(c); 4268 } 4269 break; 4270 4271 case '+': 4272 b = parseFloat(stack.pop()); 4273 a = parseFloat(stack.pop()); 4274 stack.push(a + b); 4275 break; 4276 4277 case '-': 4278 b = parseFloat(stack.pop()); 4279 a = parseFloat(stack.pop()); 4280 stack.push(a - b); 4281 break; 4282 4283 case '*': 4284 b = parseFloat(stack.pop()); 4285 a = parseFloat(stack.pop()); 4286 stack.push(a * b); 4287 break; 4288 4289 case '/': 4290 b = parseFloat(stack.pop()); 4291 a = parseFloat(stack.pop()); 4292 stack.push(a / b); 4293 break; 4294 4295 case '//': 4296 b = parseFloat(stack.pop()); 4297 a = parseFloat(stack.pop()); 4298 stack.push(parseInt(a / b)); 4299 break; 4300 4301 case '%': 4302 b = parseFloat(stack.pop()); 4303 a = parseFloat(stack.pop()); 4304 stack.push(a % b); 4305 break; 4306 4307 case '~': 4308 b = stack.pop(); 4309 a = stack.pop(); 4310 stack.push( (a != null ? a.toString() : "") 4311 + (b != null ? b.toString() : "") ); 4312 break; 4313 4314 case 'not': 4315 case '!': 4316 stack.push(!stack.pop()); 4317 break; 4318 4319 case '<': 4320 b = stack.pop(); 4321 a = stack.pop(); 4322 stack.push(a < b); 4323 break; 4324 4325 case '<=': 4326 b = stack.pop(); 4327 a = stack.pop(); 4328 stack.push(a <= b); 4329 break; 4330 4331 case '>': 4332 b = stack.pop(); 4333 a = stack.pop(); 4334 stack.push(a > b); 4335 break; 4336 4337 case '>=': 4338 b = stack.pop(); 4339 a = stack.pop(); 4340 stack.push(a >= b); 4341 break; 4342 4343 case '===': 4344 b = stack.pop(); 4345 a = stack.pop(); 4346 stack.push(a === b); 4347 break; 4348 4349 case '==': 4350 b = stack.pop(); 4351 a = stack.pop(); 4352 stack.push(a == b); 4353 break; 4354 4355 case '!==': 4356 b = stack.pop(); 4357 a = stack.pop(); 4358 stack.push(a !== b); 4359 break; 4360 4361 case '!=': 4362 b = stack.pop(); 4363 a = stack.pop(); 4364 stack.push(a != b); 4365 break; 4366 4367 case 'or': 4368 b = stack.pop(); 4369 a = stack.pop(); 4370 stack.push(a || b); 4371 break; 4372 4373 case 'and': 4374 b = stack.pop(); 4375 a = stack.pop(); 4376 stack.push(a && b); 4377 break; 4378 4379 case '**': 4380 b = stack.pop(); 4381 a = stack.pop(); 4382 stack.push(Math.pow(a, b)); 4383 break; 4384 4385 4386 case 'not in': 4387 b = stack.pop(); 4388 a = stack.pop(); 4389 stack.push( !containment(a, b) ); 4390 break; 4391 4392 case 'in': 4393 b = stack.pop(); 4394 a = stack.pop(); 4395 stack.push( containment(a, b) ); 4396 break; 4397 4398 case '..': 4399 b = stack.pop(); 4400 a = stack.pop(); 4401 stack.push( Twig.functions.range(a, b) ); 4402 break; 4403 4404 default:
4405 throw new Twig.Error(operator + " is an unknown operator."); 4406 } 4407 }; 4408 4409 return Twig; 4410 4411})( Twig || { } ); 4412// Twig.js 4413// Available under the BSD 2-Clause License 4414// https://github.com/justjohn/twig.js 4415 4416// ## twig.filters.js 4417// 4418// This file handles parsing filters. 4419var Twig = (function (Twig) { 4420 4421 // Determine object type 4422 function is(type, obj) { 4423 var clas = Object.prototype.toString.call(obj).slice(8, -1); 4424 return obj !== undefined && obj !== null && clas === type; 4425 } 4426 4427 Twig.filters = { 4428 // String Filters 4429 upper: function(value) { 4430 if ( typeof value !== "string" ) { 4431 return value; 4432 } 4433 4434 return value.toUpperCase(); 4435 }, 4436 lower: function(value) { 4437 if ( typeof value !== "string" ) { 4438 return value; 4439 } 4440 4441 return value.toLowerCase(); 4442 }, 4443 capitalize: function(value) { 4444 if ( typeof value !== "string" ) { 4445 return value; 4446 } 4447 4448 return value.substr(0, 1).toUpperCase() + value.toLowerCase().substr(1); 4449 }, 4450 title: function(value) { 4451 if ( typeof value !== "string" ) { 4452 return value; 4453 } 4454 4455 return value.toLowerCase().replace( /(^|\s)([a-z])/g , function(m, p1, p2){ 4456 return p1 + p2.toUpperCase(); 4457 }); 4458 }, 4459 length: function(value) { 4460 if (Twig.lib.is("Array", value) || typeof value === "string") { 4461 return value.length; 4462 } else if (Twig.lib.is("Object", value)) { 4463 if (value._keys === undefined) { 4464 return Object.keys(value).length; 4465 } else { 4466 return value._keys.length; 4467 } 4468 } else { 4469 return 0; 4470 } 4471 }, 4472 4473 // Array/Object Filters 4474 reverse: function(value) { 4475 if (is("Array", value)) { 4476 return value.reverse(); 4477 } else if (is("String", value)) { 4478 return value.split("").reverse().join(""); 4479 } else if (is("Object", value)) { 4480 var keys = value._keys || Object.keys(value).reverse(); 4481 value._keys = keys; 4482 return value; 4483 } 4484 }, 4485 sort: function(value) { 4486 if (is("Array", value)) { 4487 return value.sort(); 4488 } else if (is('Object', value)) { 4489 // Sorting objects isn't obvious since the order of 4490 // returned keys isn't guaranteedin JavaScript. 4491 // Because of this we use a "hidden" key called _keys to 4492 // store the keys in the order we want to return them. 4493 4494 delete value._keys; 4495 var keys = Object.keys(value), 4496 sorted_keys = keys.sort(function(a, b) { 4497 return value[a] > value[b]; 4498 }); 4499 value._keys = sorted_keys; 4500 return value; 4501 } 4502 }, 4503 keys: function(value) { 4504 if (value === undefined || value === null){ 4505 return; 4506 } 4507 4508 var keyset = value._keys || Object.keys(value), 4509 output = []; 4510 4511 Twig.forEach(keyset, function(key) { 4512 if (key === "_keys") return; // Ignore the _keys property 4513 if (value.hasOwnProperty(key)) { 4514 output.push(key); 4515 } 4516 }); 4517 return output; 4518 }, 4519 url_encode: function(value) { 4520 if (value === undefined || value === null){ 4521 return; 4522 } 4523 4524 return encodeURIComponent(value); 4525 }, 4526 join: function(value, params) { 4527 if (value === undefined || value === null){ 4528 return; 4529 } 4530 4531 var join_str = "", 4532 output = [], 4533 keyset = null; 4534 4535 if (params && params[0]) { 4536 join_str = params[0]; 4537 } 4538 if (is("Array", value)) { 4539 output = value; 4540 } else { 4541 keyset = value._keys || Object.keys(value); 4542 Twig.forEach(keyset, function(key) { 4543 if (key === "_keys") return; // Ignore the _keys property 4544 if (value.hasOwnProperty(key)) { 4545 output.push(value[key]); 4546 } 4547 }); 4548 } 4549 return output.join(join_str); 4550 }, 4551 "default": function(value, params) { 4552 if (params === undefined || params.length !== 1) {
4553 throw new Twig.Error("default filter expects one argument"); 4554 } 4555 if (value === undefined || value === null || value === '' ) { 4556 return params[0]; 4557 } else { 4558 return value; 4559 } 4560 }, 4561 json_encode: function(value) { 4562 if (value && value.hasOwnProperty( "_keys" ) ) { 4563 delete value._keys; 4564 } 4565 if(value === undefined || value === null) { 4566 return "null"; 4567 } 4568 return JSON.stringify(value); 4569 }, 4570 merge: function(value, params) { 4571 var obj = [], 4572 arr_index = 0, 4573 keyset = []; 4574 4575 // Check to see if all the objects being merged are arrays 4576 if (!is("Array", value)) { 4577 // Create obj as an Object 4578 obj = { }; 4579 } else { 4580 Twig.forEach(params, function(param) { 4581 if (!is("Array", param)) { 4582 obj = { }; 4583 } 4584 }); 4585 } 4586 if (!is("Array", obj)) { 4587 obj._keys = []; 4588 } 4589 4590 if (is("Array", value)) { 4591 Twig.forEach(value, function(val) { 4592 if (obj._keys) obj._keys.push(arr_index); 4593 obj[arr_index] = val; 4594 arr_index++; 4595 }); 4596 } else { 4597 keyset = value._keys || Object.keys(value); 4598 Twig.forEach(keyset, function(key) { 4599 obj[key] = value[key]; 4600 obj._keys.push(key); 4601 4602 // Handle edge case where a number index in an object is greater than 4603 // the array counter. In such a case, the array counter is increased 4604 // one past the index. 4605 // 4606 // Example {{ ["a", "b"]|merge({"4":"value"}, ["c", "d"])
4607 // Without this, d would have an index of "4" and overwrite the value 4608 // of "value" 4609 var int_key = parseInt(key, 10); 4610 if (!isNaN(int_key) && int_key >= arr_index) { 4611 arr_index = int_key + 1; 4612 } 4613 }); 4614 } 4615 4616 // mixin the merge arrays 4617 Twig.forEach(params, function(param) { 4618 if (is("Array", param)) { 4619 Twig.forEach(param, function(val) { 4620 if (obj._keys) obj._keys.push(arr_index); 4621 obj[arr_index] = val; 4622 arr_index++; 4623 }); 4624 } else { 4625 keyset = param._keys || Object.keys(param); 4626 Twig.forEach(keyset, function(key) { 4627 if (!obj[key]) obj._keys.push(key); 4628 obj[key] = param[key]; 4629 4630 var int_key = parseInt(key, 10); 4631 if (!isNaN(int_key) && int_key >= arr_index) { 4632 arr_index = int_key + 1; 4633 } 4634 }); 4635 } 4636 }); 4637 if (params.length === 0) { 4638 throw new Twig.Error("Filter merge expects at least one parameter"); 4639 } 4640 4641 return obj; 4642 }, 4643 date: function(value, params) { 4644 var date = Twig.functions.date(value); 4645 var format = params && params.length ? params[0] : 'F j, Y H:i'; 4646 return Twig.lib.formatDate(date, format); 4647 }, 4648 4649 date_modify: function(value, params) { 4650 if (value === undefined || value === null) { 4651 return; 4652 } 4653 if (params === undefined || params.length !== 1) { 4654 throw new Twig.Error("date_modify filter expects 1 argument"); 4655 } 4656 4657 var modifyText = params[0], time; 4658 4659 if (Twig.lib.is("Date", value)) { 4660 time = Twig.lib.strtotime(modifyText, value.getTime() / 1000); 4661 } 4662 if (Twig.lib.is("String", value)) { 4663 time = Twig.lib.strtotime(modifyText, Twig.lib.strtotime(value)); 4664 } 4665 if (Twig.lib.is("Number", value)) { 4666 time = Twig.lib.strtotime(modifyText, value); 4667 } 4668 4669 return new Date(time * 1000); 4670 }, 4671 4672 replace: function(value, params) { 4673 if (value === undefined||value === null){ 4674 return; 4675 } 4676 4677 var pairs = params[0], 4678 tag; 4679 for (tag in pairs) { 4680 if (pairs.hasOwnProperty(tag) && tag !== "_keys") { 4681 value = Twig.lib.replaceAll(value, tag, pairs[tag]); 4682 } 4683 } 4684 return value; 4685 }, 4686 4687 format: function(value, params) { 4688 if (value === undefined || value === null){ 4689 return; 4690 } 4691 4692 return Twig.lib.vsprintf(value, params); 4693 }, 4694 4695 striptags: function(value) { 4696 if (value === undefined || value === null){ 4697 return; 4698 } 4699 4700 return Twig.lib.strip_tags(value); 4701 }, 4702 4703 escape: function(value) { 4704 if (value === undefined|| value === null){ 4705 return; 4706 } 4707 var raw_value = value.toString().replace(/&/g, "&") 4708 .replace(/</g, "<") 4709 .replace(/>/g, ">") 4710 .replace(/"/g, """) 4711 .replace(/'/g, "'"); 4712 return Twig.Markup(raw_value); 4713 }, 4714 4715 /* Alias of escape */ 4716 "e": function(value) { 4717 return Twig.filters.escape(value); 4718 }, 4719 4720 nl2br: function(value) { 4721 if (value === undefined || value === null){ 4722 return; 4723 } 4724 var linebreak_tag = "BACKSLASH_n_replace", 4725 br = "<br />" + linebreak_tag; 4726 4727 value = Twig.filters.escape(value) 4728 .replace(/\r\n/g, br) 4729 .replace(/\r/g, br) 4730 .replace(/\n/g, br); 4731 4732 value = Twig.lib.replaceAll(value, linebreak_tag, "\n"); 4733 4734 return Twig.Markup(value); 4735 }, 4736 4737 /** 4738 * Adapted from: http://phpjs.org/functions/number_format:481 4739 */ 4740 number_format: function(value, params) { 4741 var number = value, 4742 decimals = (params && params[0]) ? params[0] : undefined, 4743 dec = (params && params[1] !== undefined) ? params[1] : ".", 4744 sep = (params && params[2] !== undefined) ? params[2] : ","; 4745 4746 number = (number + '').replace(/[^0-9+\-Ee.]/g, ''); 4747 var n = !isFinite(+number) ? 0 : +number, 4748 prec = !isFinite(+decimals) ? 0 : Math.abs(decimals), 4749 s = '', 4750 toFixedFix = function (n, prec) { 4751 var k = Math.pow(10, prec); 4752 return '' + Math.round(n * k) / k; 4753 }; 4754 // Fix for IE parseFloat(0.55).toFixed(0) = 0; 4755 s = (prec ? toFixedFix(n, prec) : '' + Math.round(n)).split('.'); 4756 if (s[0].length > 3) { 4757 s[0] = s[0].replace(/\B(?=(?:\d{3})+(?!\d))/g, sep); 4758 } 4759 if ((s[1] || '').length < prec) { 4760 s[1] = s[1] || ''; 4761 s[1] += new Array(prec - s[1].length + 1).join('0'); 4762 } 4763 return s.join(dec); 4764 }, 4765 4766 trim: function(value, params) { 4767 if (value === undefined|| value === null){ 4768 return; 4769 } 4770 4771 var str = Twig.filters.escape( '' + value ), 4772 whitespace; 4773 if ( params && params[0] ) { 4774 whitespace = '' + params[0]; 4775 } else { 4776 whitespace = ' \n\r\t\f\x0b\xa0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u3000'; 4777 } 4778 for (var i = 0; i < str.length; i++) { 4779 if (whitespace.indexOf(str.charAt(i)) === -1) { 4780 str = str.substring(i); 4781 break; 4782 } 4783 } 4784 for (i = str.length - 1; i >= 0; i--) { 4785 if (whitespace.indexOf(str.charAt(i)) === -1) { 4786 str = str.substring(0, i + 1); 4787 break; 4788 } 4789 } 4790 return whitespace.indexOf(str.charAt(0)) === -1 ? str : ''; 4791 }, 4792 4793 truncate: function (value, params) { 4794 var length = 30, 4795 preserve = false, 4796 separator = '...'; 4797 4798 value = value + ''; 4799 if (params) { 4800 if (params[0]) { 4801 length = params[0]; 4802 } 4803 if (params[1]) { 4804 preserve = params[1]; 4805 } 4806 if (params[2]) { 4807 separator = params[2]; 4808 } 4809 } 4810 4811 if (value.length > length) { 4812 4813 if (preserve) { 4814 length = value.indexOf(' ', length); 4815 if (length === -1) { 4816 return value; 4817 } 4818 } 4819 4820 value = value.substr(0, length) + separator; 4821 } 4822 4823 return value; 4824 }, 4825 4826 slice: function(value, params) { 4827 if (value === undefined || value === null) { 4828 return; 4829 } 4830 if (params === undefined || params.length < 1) {
4831 throw new Twig.Error("slice filter expects at least 1 argument"); 4832 } 4833 4834 // default to start of string 4835 var start = params[0] || 0; 4836 // default to length of string 4837 var length = params.length > 1 ? params[1] : value.length; 4838 // handle negative start values 4839 var startIndex = start >= 0 ? start : Math.max( value.length + start, 0 ); 4840 4841 if (Twig.lib.is("Array", value)) { 4842 var output = []; 4843 for (var i = startIndex; i < startIndex + length && i < value.length; i++) { 4844 output.push(value[i]); 4845 } 4846 return output; 4847 } else if (Twig.lib.is("String", value)) { 4848 return value.substr(startIndex, length); 4849 } else { 4850 throw new Twig.Error("slice filter expects value to be an array or string"); 4851 } 4852 }, 4853 4854 abs: function(value) { 4855 if (value === undefined || value === null) { 4856 return; 4857 } 4858 4859 return Math.abs(value); 4860 }, 4861 4862 first: function(value) { 4863 if (is("Array", value)) { 4864 return value[0]; 4865 } else if (is("Object", value)) { 4866 if ('_keys' in value) { 4867 return value[value._keys[0]]; 4868 } 4869 } else if ( typeof value === "string" ) { 4870 return value.substr(0, 1); 4871 } 4872 4873 return; 4874 }, 4875 4876 split: function(value, params) { 4877 if (value === undefined || value === null) { 4878 return; 4879 } 4880 if (params === undefined || params.length < 1 || params.length > 2) { 4881 throw new Twig.Error("split filter expects 1 or 2 argument"); 4882 } 4883 if (Twig.lib.is("String", value)) { 4884 var delimiter = params[0], 4885 limit = params[1], 4886 split = value.split(delimiter); 4887 4888 if (limit === undefined) { 4889 4890 return split; 4891 4892 } else if (limit < 0) { 4893 4894 return value.split(delimiter, split.length + limit); 4895 4896 } else { 4897 4898 var limitedSplit = []; 4899 4900 if (delimiter == '') { 4901 // empty delimiter 4902 // "aabbcc"|split('', 2) 4903 // -> ['aa', 'bb', 'cc'] 4904 4905 while(split.length > 0) { 4906 var temp = ""; 4907 for (var i=0; i<limit && split.length > 0; i++) { 4908 temp += split.shift(); 4909 } 4910 limitedSplit.push(temp); 4911 } 4912 4913 } else { 4914 // non-empty delimiter 4915 // "one,two,three,four,five"|split(',', 3) 4916 // -> ['one', 'two', 'three,four,five'] 4917 4918 for (var i=0; i<limit-1 && split.length > 0; i++) { 4919 limitedSplit.push(split.shift()); 4920 } 4921 4922 if (split.length > 0) { 4923 limitedSplit.push(split.join(delimiter)); 4924 } 4925 } 4926 4927 return limitedSplit; 4928 } 4929 4930 } else { 4931 throw new Twig.Error("split filter expects value to be a string"); 4932 } 4933 }, 4934 last: function(value) { 4935 if (Twig.lib.is('Object', value)) { 4936 var keys; 4937 4938 if (value._keys === undefined) { 4939 keys = Object.keys(value); 4940 } else { 4941 keys = value._keys; 4942 } 4943 4944 return value[keys[keys.length - 1]]; 4945 } 4946 4947 // string|array 4948 return value[value.length - 1]; 4949 }, 4950 raw: function(value) { 4951 return Twig.Markup(value); 4952 }, 4953 batch: function(items, params) { 4954 var size = params.shift(), 4955 fill = params.shift(), 4956 result, 4957 last, 4958 missing; 4959 4960 if (!Twig.lib.is("Array", items)) {
4961 throw new Twig.Error("batch filter expects items to be an array"); 4962 } 4963 4964 if (!Twig.lib.is("Number", size)) { 4965 throw new Twig.Error("batch filter expects size to be a number"); 4966 } 4967 4968 size = Math.ceil(size); 4969 4970 result = Twig.lib.chunkArray(items, size); 4971 4972 if (fill && items.length % size != 0) { 4973 last = result.pop(); 4974 missing = size - last.length; 4975 4976 while (missing--) { 4977 last.push(fill); 4978 } 4979 4980 result.push(last); 4981 } 4982 4983 return result; 4984 }, 4985 round: function(value, params) { 4986 params = params || []; 4987 4988 var precision = params.length > 0 ? params[0] : 0, 4989 method = params.length > 1 ? params[1] : "common"; 4990 4991 value = parseFloat(value); 4992 4993 if(precision && !Twig.lib.is("Number", precision)) { 4994 throw new Twig.Error("round filter expects precision to be a number"); 4995 } 4996 4997 if (method === "common") { 4998 return Twig.lib.round(value, precision); 4999 } 5000 5001 if(!Twig.lib.is("Function", Math[method])) { 5002 throw new Twig.Error("round filter expects method to be 'floor', 'ceil', or 'common'"); 5003 } 5004 5005 return Math[method](value * Math.pow(10, precision)) / Math.pow(10, precision); 5006 } 5007 }; 5008 5009 Twig.filter = function(filter, value, params) { 5010 if (!Twig.filters[filter]) { 5011 throw "Unable to find filter " + filter; 5012 } 5013 return Twig.filters[filter].apply(this, [value, params]); 5014 }; 5015 5016 Twig.filter.extend = function(filter, definition) { 5017 Twig.filters[filter] = definition; 5018 }; 5019 5020 return Twig; 5021 5022})(Twig || { }); 5023// Twig.js 5024// 2012 Hadrien Lanneau 5025// Available under the BSD 2-Clause License 5026// https://github.com/justjohn/twig.js 5027 5028// ## twig.functions.js 5029// 5030// This file handles parsing filters. 5031var Twig = (function (Twig) { 5032 5033 // Determine object type 5034 function is(type, obj) { 5035 var clas = Object.prototype.toString.call(obj).slice(8, -1); 5036 return obj !== undefined && obj !== null && clas === type; 5037 } 5038 5039 Twig.functions = { 5040 // attribute, block, constant, date, dump, parent, random,. 5041 5042 // Range function from http://phpjs.org/functions/range:499 5043 // Used under an MIT License 5044 range: function (low, high, step) { 5045 // http://kevin.vanzonneveld.net 5046 // + original by: Waldo Malqui Silva 5047 // * example 1: range ( 0, 12 ); 5048 // * returns 1: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] 5049 // * example 2: range( 0, 100, 10 ); 5050 // * returns 2: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100] 5051 // * example 3: range( 'a', 'i' ); 5052 // * returns 3: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'] 5053 // * example 4: range( 'c', 'a' ); 5054 // * returns 4: ['c', 'b', 'a'] 5055 var matrix = []; 5056 var inival, endval, plus; 5057 var walker = step || 1; 5058 var chars = false; 5059 5060 if (!isNaN(low) && !isNaN(high)) { 5061 inival = parseInt(low, 10); 5062 endval = parseInt(high, 10); 5063 } else if (isNaN(low) && isNaN(high)) { 5064 chars = true; 5065 inival = low.charCodeAt(0); 5066 endval = high.charCodeAt(0); 5067 } else { 5068 inival = (isNaN(low) ? 0 : low); 5069 endval = (isNaN(high) ? 0 : high); 5070 } 5071 5072 plus = ((inival > endval) ? false : true); 5073 if (plus) { 5074 while (inival <= endval) { 5075 matrix.push(((chars) ? String.fromCharCode(inival) : inival)); 5076 inival += walker; 5077 } 5078 } else { 5079 while (inival >= endval) { 5080 matrix.push(((chars) ? String.fromCharCode(inival) : inival)); 5081 inival -= walker; 5082 } 5083 } 5084 5085 return matrix; 5086 }, 5087 cycle: function(arr, i) { 5088 var pos = i % arr.length; 5089 return arr[pos]; 5090 }, 5091 dump: function() { 5092 var EOL = '\n', 5093 indentChar = ' ', 5094 indentTimes = 0, 5095 out = '', 5096 args = Array.prototype.slice.call(arguments), 5097 indent = function(times) { 5098 var ind = ''; 5099 while (times > 0) { 5100 times--; 5101 ind += indentChar; 5102 } 5103 return ind; 5104 }, 5105 displayVar = function(variable) { 5106 out += indent(indentTimes); 5107 if (typeof(variable) === 'object') { 5108 dumpVar(variable); 5109 } else if (typeof(variable) === 'function') { 5110 out += 'function()' + EOL; 5111 } else if (typeof(variable) === 'string') { 5112 out += 'string(' + variable.length + ') "' + variable + '"' + EOL; 5113 } else if (typeof(variable) === 'number') { 5114 out += 'number(' + variable + ')' + EOL; 5115 } else if (typeof(variable) === 'boolean') { 5116 out += 'bool(' + variable + ')' + EOL; 5117 } 5118 }, 5119 dumpVar = function(variable) { 5120 var i; 5121 if (variable === null) { 5122 out += 'NULL' + EOL; 5123 } else if (variable === undefined) { 5124 out += 'undefined' + EOL; 5125 } else if (typeof variable === 'object') { 5126 out += indent(indentTimes) + typeof(variable); 5127 indentTimes++; 5128 out += '(' + (function(obj) { 5129 var size = 0, key; 5130 for (key in obj) { 5131 if (obj.hasOwnProperty(key)) { 5132 size++; 5133 } 5134 } 5135 return size; 5136 })(variable) + ') {' + EOL; 5137 for (i in variable) { 5138 out += indent(indentTimes) + '[' + i + ']=> ' + EOL; 5139 displayVar(variable[i]); 5140 } 5141 indentTimes--; 5142 out += indent(indentTimes) + '}' + EOL; 5143 } else { 5144 displayVar(variable); 5145 } 5146 }; 5147 5148 // handle no argument case by dumping the entire render context 5149 if (args.length == 0) args.push(this.context); 5150 5151 Twig.forEach(args, function(variable) { 5152 dumpVar(variable); 5153 }); 5154 5155 return out; 5156 }, 5157 date: function(date, time) { 5158 var dateObj; 5159 if (date === undefined) { 5160 dateObj = new Date(); 5161 } else if (Twig.lib.is("Date", date)) { 5162 dateObj = date; 5163 } else if (Twig.lib.is("String", date)) {
5164 dateObj = new Date(Twig.lib.strtotime(date) * 1000); 5165 } else if (Twig.lib.is("Number", date)) { 5166 // timestamp 5167 dateObj = new Date(date * 1000); 5168 } else { 5169 throw new Twig.Error("Unable to parse date " + date); 5170 } 5171 return dateObj; 5172 }, 5173 block: function(block) { 5174 if (this.originalBlockTokens[block]) { 5175 return Twig.logic.parse.apply(this, [this.originalBlockTokens[block], this.context]).output; 5176 } else { 5177 return this.blocks[block]; 5178 } 5179 }, 5180 parent: function() { 5181 // Add a placeholder 5182 return Twig.placeholders.parent; 5183 }, 5184 attribute: function(object, method, params) { 5185 if (Twig.lib.is('Object', object)) { 5186 if (object.hasOwnProperty(method)) { 5187 if (typeof object[method] === "function") { 5188 return object[method].apply(undefined, params); 5189 } 5190 else { 5191 return object[method]; 5192 } 5193 } 5194 } 5195 // Array will return element 0-index 5196 return object[method] || undefined; 5197 }, 5198 template_from_string: function(template) { 5199 if (template === undefined) { 5200 template = ''; 5201 } 5202 return new Twig.Template({ 5203 options: this.options, 5204 data: template 5205 }); 5206 }, 5207 random: function(value) { 5208 var LIMIT_INT31 = 0x80000000; 5209 5210 function getRandomNumber(n) { 5211 var random = Math.floor(Math.random() * LIMIT_INT31); 5212 var limits = [0, n]; 5213 var min = Math.min.apply(null, limits), 5214 max = Math.max.apply(null, limits); 5215 return min + Math.floor((max - min + 1) * random / LIMIT_INT31); 5216 } 5217 5218 if(Twig.lib.is("Number", value)) { 5219 return getRandomNumber(value); 5220 } 5221 5222 if(Twig.lib.is("String", value)) { 5223 return value.charAt(getRandomNumber(value.length-1)); 5224 } 5225 5226 if(Twig.lib.is("Array", value)) { 5227 return value[getRandomNumber(value.length-1)]; 5228 } 5229 5230 if(Twig.lib.is("Object", value)) { 5231 var keys = Object.keys(value); 5232 return value[keys[getRandomNumber(keys.length-1)]]; 5233 } 5234 5235 return getRandomNumber(LIMIT_INT31-1); 5236 } 5237 }; 5238 5239 Twig._function = function(_function, value, params) { 5240 if (!Twig.functions[_function]) { 5241 throw "Unable to find function " + _function; 5242 } 5243 return Twig.functions[_function](value, params); 5244 }; 5245 5246 Twig._function.extend = function(_function, definition) { 5247 Twig.functions[_function] = definition; 5248 }; 5249 5250 return Twig; 5251 5252})(Twig || { }); 5253// Twig.js 5254// Available under the BSD 2-Clause License 5255// https://github.com/justjohn/twig.js 5256 5257// ## twig.tests.js 5258// 5259// This file handles expression tests. (is empty, is not defined, etc...) 5260var Twig = (function (Twig) { 5261 "use strict"; 5262 Twig.tests = { 5263 empty: function(value) { 5264 if (value === null || value === undefined) return true; 5265 // Handler numbers 5266 if (typeof value === "number") return false; // numbers are never "empty" 5267 // Handle strings and arrays 5268 if (value.length && value.length > 0) return false; 5269 // Handle objects 5270 for (var key in value) { 5271 if (value.hasOwnProperty(key)) return false; 5272 } 5273 return true; 5274 }, 5275 odd: function(value) { 5276 return value % 2 === 1; 5277 }, 5278 even: function(value) { 5279 return value % 2 === 0; 5280 }, 5281 divisibleby: function(value, params) { 5282 return value % params[0] === 0; 5283 }, 5284 defined: function(value) { 5285 return value !== undefined; 5286 }, 5287 none: function(value) { 5288 return value === null; 5289 }, 5290 'null': function(value) { 5291 return this.none(value); // Alias of none 5292 }, 5293 sameas: function(value, params) { 5294 return value === params[0]; 5295 }, 5296 iterable: function(value) { 5297 return value && (Twig.lib.is("Array", value) || Twig.lib.is("Object", value)); 5298 } 5299 /* 5300 constant ? 5301 */ 5302 }; 5303 5304 Twig.test = function(test, value, params) { 5305 if (!Twig.tests[test]) { 5306 throw "Test " + test + " is not defined."; 5307 } 5308 return Twig.tests[test](value, params); 5309 }; 5310 5311 Twig.test.extend = function(test, definition) { 5312 Twig.tests[test] = definition; 5313 }; 5314 5315 return Twig; 5316})( Twig || { } ); 5317// Twig.js 5318// Available under the BSD 2-Clause License 5319// https://github.com/justjohn/twig.js 5320 5321// ## twig.exports.js 5322// 5323// This file provides extension points and other hooks into the twig functionality. 5324 5325var Twig = (function (Twig) { 5326 "use strict"; 5327 Twig.exports = { 5328 VERSION: Twig.VERSION 5329 }; 5330 5331 /** 5332 * Create and compile a twig.js template. 5333 * 5334 * @param {Object} param Paramteres for creating a Twig template. 5335 * 5336 * @return {Twig.Template} A Twig template ready for rendering. 5337 */ 5338 Twig.exports.twig = function twig(params) { 5339 'use strict'; 5340 var id = params.id, 5341 options = {
5342 strict_variables: params.strict_variables || false, 5343 // TODO: turn autoscape on in the next major version 5344 autoescape: params.autoescape != null && params.autoescape || false, 5345 allowInlineIncludes: params.allowInlineIncludes || false, 5346 rethrow: params.rethrow || false 5347 }; 5348 5349 if (id) { 5350 Twig.validateId(id); 5351 } 5352 5353 if (params.debug !== undefined) { 5354 Twig.debug = params.debug; 5355 } 5356 if (params.trace !== undefined) { 5357 Twig.trace = params.trace; 5358 } 5359 5360 if (params.data !== undefined) { 5361 return new Twig.Template({ 5362 data: params.data, 5363 module: params.module, 5364 id: id, 5365 options: options 5366 }); 5367 5368 } else if (params.ref !== undefined) { 5369 if (params.id !== undefined) { 5370 throw new Twig.Error("Both ref and id cannot be set on a twig.js template."); 5371 } 5372 return Twig.Templates.load(params.ref); 5373 5374 } else if (params.href !== undefined) { 5375 return Twig.Templates.loadRemote(params.href, { 5376 id: id, 5377 method: 'ajax', 5378 base: params.base, 5379 module: params.module, 5380 precompiled: params.precompiled, 5381 async: params.async, 5382 options: options 5383 5384 }, params.load, params.error); 5385 5386 } else if (params.path !== undefined) { 5387 return Twig.Templates.loadRemote(params.path, { 5388 id: id, 5389 method: 'fs', 5390 base: params.base, 5391 module: params.module, 5392 precompiled: params.precompiled, 5393 async: params.async, 5394 options: options 5395 5396 }, params.load, params.error); 5397 } 5398 }; 5399 5400 // Extend Twig with a new filter. 5401 Twig.exports.extendFilter = function(filter, definition) { 5402 Twig.filter.extend(filter, definition); 5403 }; 5404 5405 // Extend Twig with a new function. 5406 Twig.exports.extendFunction = function(fn, definition) { 5407 Twig._function.extend(fn, definition); 5408 }; 5409 5410 // Extend Twig with a new test. 5411 Twig.exports.extendTest = function(test, definition) { 5412 Twig.test.extend(test, definition); 5413 }; 5414 5415 // Extend Twig with a new definition. 5416 Twig.exports.extendTag = function(definition) { 5417 Twig.logic.extend(definition); 5418 }; 5419 5420 // Provide an environment for extending Twig core. 5421 // Calls fn with the internal Twig object. 5422 Twig.exports.extend = function(fn) { 5423 fn(Twig); 5424 }; 5425 5426 5427 /** 5428 * Provide an extension for use with express 2. 5429 * 5430 * @param {string} markup The template markup. 5431 * @param {array} options The express options. 5432 * 5433 * @return {string} The rendered template. 5434 */ 5435 Twig.exports.compile = function(markup, options) { 5436 var id = options.filename, 5437 path = options.filename, 5438 template; 5439 5440 // Try to load the template from the cache 5441 template = new Twig.Template({ 5442 data: markup, 5443 path: path, 5444 id: id, 5445 options: options.settings['twig options'] 5446 }); // Twig.Templates.load(id) || 5447 5448 return function(context) { 5449 return template.render(context); 5450 }; 5451 }; 5452 5453 /** 5454 * Provide an extension for use with express 3. 5455 * 5456 * @param {string} path The location of the template file on disk. 5457 * @param {Object|Function} The options or callback. 5458 * @param {Function} fn callback. 5459 */ 5460 5461 Twig.exports.renderFile = function(path, options, fn) { 5462 // handle callback in options 5463 if ('function' == typeof options) { 5464 fn = options; 5465 options = {}; 5466 } 5467 5468 options = options || {}; 5469 5470 var params = { 5471 path: path, 5472 base: options.settings['views'], 5473 load: function(template) { 5474 // render and return template 5475 fn(null, template.render(options)); 5476 } 5477 }; 5478 5479 // mixin any options provided to the express app. 5480 var view_options = options.settings['twig options']; 5481 5482 if (view_options) { 5483 for (var option in view_options) if (view_options.hasOwnProperty(option)) { 5484 params[option] = view_options[option]; 5485 } 5486 } 5487 5488 Twig.exports.twig(params); 5489 }; 5490 5491 // Express 3 handler 5492 Twig.exports.__express = Twig.exports.renderFile; 5493 5494 /** 5495 * Shoud Twig.js cache templates. 5496 * Disable during development to see changes to templates without 5497 * reloading, and disable in production to improve performance. 5498 * 5499 * @param {boolean} cache 5500 */ 5501 Twig.exports.cache = function(cache) { 5502 Twig.cache = cache; 5503 } 5504 5505 return Twig; 5506}) (Twig || { }); 5507 5508// Twig.js 5509// Available under the BSD 2-Clause License 5510// https://github.com/justjohn/twig.js 5511 5512// ## twig.compiler.js 5513// 5514// This file handles compiling templates into JS 5515var Twig = (function (Twig) { 5516 /** 5517 * Namespace for compilation. 5518 */ 5519 Twig.compiler = { 5520 module: {} 5521 }; 5522 5523 // Compile a Twig Template to output. 5524 Twig.compiler.compile = function(template, options) { 5525 // Get tokens 5526 var tokens = JSON.stringify(template.tokens) 5527 , id = template.id 5528 , output; 5529 5530 if (options.module) { 5531 if (Twig.compiler.module[options.module] === undefined) {
5532 throw new Twig.Error("Unable to find module type " + options.module); 5533 } 5534 output = Twig.compiler.module[options.module](id, tokens, options.twig); 5535 } else { 5536 output = Twig.compiler.wrap(id, tokens); 5537 } 5538 return output; 5539 }; 5540 5541 Twig.compiler.module = { 5542 amd: function(id, tokens, pathToTwig) { 5543 return 'define(["' + pathToTwig + '"], function (Twig) {\n\tvar twig, templates;\ntwig = Twig.twig;\ntemplates = ' + Twig.compiler.wrap(id, tokens) + '\n\treturn templates;\n});'; 5544 } 5545 , node: function(id, tokens) { 5546 return 'var twig = require("twig").twig;\n' 5547 + 'exports.template = ' + Twig.compiler.wrap(id, tokens) 5548 } 5549 , cjs2: function(id, tokens, pathToTwig) { 5550 return 'module.declare([{ twig: "' + pathToTwig + '" }], function (require, exports, module) {\n' 5551 + '\tvar twig = require("twig").twig;\n' 5552 + '\texports.template = ' + Twig.compiler.wrap(id, tokens) 5553 + '\n});' 5554 } 5555 }; 5556 5557 Twig.compiler.wrap = function(id, tokens) { 5558 return 'twig({id:"'+id.replace('"', '\\"')+'", data:'+tokens+', precompiled: true});\n'; 5559 }; 5560 5561 return Twig; 5562})(Twig || {}); 5563// Twig.js 5564// Available under the BSD 2-Clause License 5565// https://github.com/justjohn/twig.js 5566 5567// ## twig.module.js 5568// Provide a CommonJS/AMD/Node module export. 5569 5570if (typeof module !== 'undefined' && module.declare) { 5571 // Provide a CommonJS Modules/2.0 draft 8 module 5572 module.declare([], function(require, exports, module) { 5573 // Add exports from the Twig exports 5574 for (key in Twig.exports) { 5575 if (Twig.exports.hasOwnProperty(key)) { 5576 exports[key] = Twig.exports[key]; 5577 } 5578 } 5579 }); 5580} else if (typeof define == 'function' && define.amd) { 5581 define(function() { 5582 return Twig.exports; 5583 }); 5584} else if (typeof module !== 'undefined' && module.exports) { 5585 // Provide a CommonJS Modules/1.1 module 5586 module.exports = Twig.exports; 5587} else { 5588 // Export for browser use 5589 window.twig = Twig.exports.twig; 5590 window.Twig = Twig.exports; 5591} 5592
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.