UNPKG

twig

Version:

JS port of the Twig templating language.

1,541 lines (1,446 loc) 291 kB
(function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define([], factory); else if(typeof exports === 'object') exports["Twig"] = factory(); else root["Twig"] = factory(); })(global, () => { return /******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ({ /***/ 683 (module) { "use strict"; // ## twig.async.js // // This file handles asynchronous tasks within twig. module.exports = function (Twig) { 'use strict'; var STATE_UNKNOWN = 0; var STATE_RESOLVED = 1; var STATE_REJECTED = 2; Twig.ParseState.prototype.parseAsync = function (tokens, context) { return this.parse(tokens, context, true); }; Twig.expression.parseAsync = function (tokens, context, tokensAreParameters) { var state = this; return Twig.expression.parse.call(state, tokens, context, tokensAreParameters, true); }; Twig.logic.parseAsync = function (token, context, chain) { var state = this; return Twig.logic.parse.call(state, token, context, chain, true); }; Twig.Template.prototype.renderAsync = function (context, params) { return this.render(context, params, true); }; Twig.async = {}; /** * Checks for `thenable` objects */ Twig.isPromise = function (obj) { return obj && obj.then && typeof obj.then === 'function'; }; /** * Handling of code paths that might either return a promise * or a value depending on whether async code is used. * * @see https://github.com/twigjs/twig.js/blob/master/ASYNC.md#detecting-asynchronous-behaviour */ function potentiallyAsyncSlow(that, allowAsync, action) { var result = action.call(that); var err = null; var isAsync = true; if (!Twig.isPromise(result)) { return result; } result.then(function (res) { result = res; isAsync = false; })["catch"](function (error) { err = error; }); if (err !== null) { throw err; } if (isAsync) { throw new Twig.Error('You are using Twig.js in sync mode in combination with async extensions.'); } return result; } Twig.async.potentiallyAsync = function (that, allowAsync, action) { if (allowAsync) { return Twig.Promise.resolve(action.call(that)); } return potentiallyAsyncSlow(that, allowAsync, action); }; function run(fn, resolve, reject) { try { fn(resolve, reject); } catch (error) { reject(error); } } function pending(handlers, onResolved, onRejected) { var h = [onResolved, onRejected, -2]; // The promise has yet to be rejected or resolved. if (!handlers) { handlers = h; } else if (handlers[2] === -2) { // Only allocate an array when there are multiple handlers handlers = [handlers, h]; } else { handlers.push(h); } return handlers; } /** * Really small thenable to represent promises that resolve immediately. * */ Twig.Thenable = function (then, value, state) { this.then = then; this._value = state ? value : null; this._state = state || STATE_UNKNOWN; }; Twig.Thenable.prototype["catch"] = function (onRejected) { // THe promise will not throw, it has already resolved. if (this._state === STATE_RESOLVED) { return this; } return this.then(null, onRejected); }; /** * The `then` method attached to a Thenable when it has resolved. * */ Twig.Thenable.resolvedThen = function (onResolved) { try { return Twig.Promise.resolve(onResolved(this._value)); } catch (error) { return Twig.Promise.reject(error); } }; /** * The `then` method attached to a Thenable when it has rejected. * */ Twig.Thenable.rejectedThen = function (onResolved, onRejected) { // Shortcut for rejected twig promises if (!onRejected || typeof onRejected !== 'function') { return this; } var value = this._value; var result; try { result = onRejected(value); } catch (error) { result = Twig.Promise.reject(error); } return Twig.Promise.resolve(result); }; /** * An alternate implementation of a Promise that does not fully follow * the spec, but instead works fully synchronous while still being * thenable. * * These promises can be mixed with regular promises at which point * the synchronous behaviour is lost. */ Twig.Promise = function (executor) { var state = STATE_UNKNOWN; var value = null; var changeState = function changeState(nextState, nextValue) { state = nextState; value = nextValue; }; function onReady(v) { changeState(STATE_RESOLVED, v); } function onReject(e) { changeState(STATE_REJECTED, e); } run(executor, onReady, onReject); // If the promise settles right after running the executor we can // return a Promise with it's state already set. // // Twig.Promise.resolve and Twig.Promise.reject both use the more // efficient `Twig.Thenable` for this purpose. if (state === STATE_RESOLVED) { return Twig.Promise.resolve(value); } if (state === STATE_REJECTED) { return Twig.Promise.reject(value); } // If we managed to get here our promise is going to resolve asynchronous. changeState = new Twig.FullPromise(); return changeState.promise; }; /** * Promise implementation that can handle being resolved at any later time. * */ Twig.FullPromise = function () { var handlers = null; // The state has been changed to either resolve, or reject // which means we should call the handler. function resolved(onResolved) { onResolved(p._value); } function rejected(onResolved, onRejected) { onRejected(p._value); } var append = function append(onResolved, onRejected) { handlers = pending(handlers, onResolved, onRejected); }; function changeState(newState, v) { if (p._state) { return; } p._value = v; p._state = newState; append = newState === STATE_RESOLVED ? resolved : rejected; if (!handlers) { return; } if (handlers[2] === -2) { append(handlers[0], handlers[1]); handlers = null; return; } handlers.forEach(function (h) { append(h[0], h[1]); }); handlers = null; } var p = new Twig.Thenable(function (onResolved, onRejected) { var hasResolved = typeof onResolved === 'function'; // Shortcut for resolved twig promises if (p._state === STATE_RESOLVED && !hasResolved) { return Twig.Promise.resolve(p._value); } if (p._state === STATE_RESOLVED) { try { return Twig.Promise.resolve(onResolved(p._value)); } catch (error) { return Twig.Promise.reject(error); } } var hasRejected = typeof onRejected === 'function'; return new Twig.Promise(function (resolve, reject) { append(hasResolved ? function (result) { try { resolve(onResolved(result)); } catch (error) { reject(error); } } : resolve, hasRejected ? function (err) { try { resolve(onRejected(err)); } catch (error) { reject(error); } } : reject); }); }); changeState.promise = p; return changeState; }; Twig.Promise.defaultResolved = new Twig.Thenable(Twig.Thenable.resolvedThen, undefined, STATE_RESOLVED); Twig.Promise.emptyStringResolved = new Twig.Thenable(Twig.Thenable.resolvedThen, '', STATE_RESOLVED); Twig.Promise.resolve = function (value) { if (arguments.length === 0 || typeof value === 'undefined') { return Twig.Promise.defaultResolved; } if (Twig.isPromise(value)) { return value; } // Twig often resolves with an empty string, we optimize for this // scenario by returning a fixed promise. This reduces the load on // garbage collection. if (value === '') { return Twig.Promise.emptyStringResolved; } return new Twig.Thenable(Twig.Thenable.resolvedThen, value, STATE_RESOLVED); }; Twig.Promise.reject = function (e) { // `e` should never be a promise. return new Twig.Thenable(Twig.Thenable.rejectedThen, e, STATE_REJECTED); }; Twig.Promise.all = function (promises) { var results = new Array(promises.length); return Twig.async.forEach(promises, function (p, index) { if (!Twig.isPromise(p)) { results[index] = p; return; } if (p._state === STATE_RESOLVED) { results[index] = p._value; return; } return p.then(function (v) { results[index] = v; }); }).then(function () { return results; }); }; /** * Go over each item in a fashion compatible with Twig.forEach, * allow the function to return a promise or call the third argument * to signal it is finished. * * Each item in the array will be called sequentially. */ Twig.async.forEach = function (arr, callback) { var len = arr ? arr.length : 0; var index = 0; function next() { var resp = null; do { if (index === len) { return Twig.Promise.resolve(); } resp = callback(arr[index], index); index++; // While the result of the callback is not a promise or it is // a promise that has settled we can use a regular loop which // is much faster. } while (!resp || !Twig.isPromise(resp) || resp._state === STATE_RESOLVED); return resp.then(next); } return next(); }; return Twig; }; /***/ }, /***/ 198 (module) { "use strict"; // ## twig.compiler.js // // This file handles compiling templates into JS module.exports = function (Twig) { /** * Namespace for compilation. */ Twig.compiler = { module: {} }; // Compile a Twig Template to output. Twig.compiler.compile = function (template, options) { // Get tokens var tokens = JSON.stringify(template.tokens); var id = template.id; var output = null; if (options.module) { if (Twig.compiler.module[options.module] === undefined) { throw new Twig.Error('Unable to find module type ' + options.module); } output = Twig.compiler.module[options.module](id, tokens, options.twig); } else { output = Twig.compiler.wrap(id, tokens); } return output; }; Twig.compiler.module = { amd: function amd(id, tokens, pathToTwig) { return 'define(["' + pathToTwig + '"], function (Twig) {\n\tvar twig, templates;\ntwig = Twig.twig;\ntemplates = ' + Twig.compiler.wrap(id, tokens) + '\n\treturn templates;\n});'; }, node: function node(id, tokens) { return 'var twig = require("twig").twig;\nexports.template = ' + Twig.compiler.wrap(id, tokens); }, cjs2: function cjs2(id, tokens, pathToTwig) { return 'module.declare([{ twig: "' + pathToTwig + '" }], function (require, exports, module) {\n\tvar twig = require("twig").twig;\n\texports.template = ' + Twig.compiler.wrap(id, tokens) + '\n});'; } }; Twig.compiler.wrap = function (id, tokens) { return 'twig({id:"' + id.replace('"', '\\"') + '", data:' + tokens + ', precompiled: true});\n'; }; return Twig; }; /***/ }, /***/ 636 (module, __unused_webpack_exports, __webpack_require__) { "use strict"; var _interopRequireDefault = __webpack_require__(994); var _defineProperty2 = _interopRequireDefault(__webpack_require__(693)); function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2["default"])(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } // ## twig.core.js // // This file handles template level tokenizing, compiling and parsing. module.exports = function (Twig) { 'use strict'; Twig.trace = false; Twig.debug = false; // Default caching to true for the improved performance it offers Twig.cache = true; Twig.noop = function () {}; Twig.merge = function (target, source, onlyChanged) { Object.keys(source).forEach(function (key) { if (onlyChanged && !(key in target)) { return; } target[key] = source[key]; }); return target; }; /** * Exception thrown by twig.js. */ Twig.Error = function (message, file) { this.message = message; this.name = 'TwigException'; this.type = 'TwigException'; this.file = file; }; /** * Get the string representation of a Twig error. */ Twig.Error.prototype.toString = function () { var output = this.name + ': ' + this.message; return output; }; /** * Wrapper for logging to the console. */ Twig.log = { trace: function trace() { if (Twig.trace && console) { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } console.log(Array.prototype.slice.call(args)); } }, debug: function debug() { if (Twig.debug && console) { for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { args[_key2] = arguments[_key2]; } console.log(Array.prototype.slice.call(args)); } } }; if (typeof console === 'undefined') { Twig.log.error = function () {}; } else if (typeof console.error !== 'undefined') { Twig.log.error = function () { var _console; (_console = console).error.apply(_console, arguments); }; } else if (typeof console.log !== 'undefined') { Twig.log.error = function () { var _console2; (_console2 = console).log.apply(_console2, arguments); }; } /** * Container for methods related to handling high level template tokens * (for example: {{ expression }}, {% logic %}, {# comment #}, raw data) */ Twig.token = {}; /** * Token types. */ Twig.token.type = { output: 'output', logic: 'logic', comment: 'comment', raw: 'raw', outputWhitespacePre: 'output_whitespace_pre', outputWhitespacePost: 'output_whitespace_post', outputWhitespaceBoth: 'output_whitespace_both', logicWhitespacePre: 'logic_whitespace_pre', logicWhitespacePost: 'logic_whitespace_post', logicWhitespaceBoth: 'logic_whitespace_both' }; /** * Token syntax definitions. */ Twig.token.definitions = [{ type: Twig.token.type.raw, open: '{% raw %}', close: '{% endraw %}' }, { type: Twig.token.type.raw, open: '{% verbatim %}', close: '{% endverbatim %}' }, // *Whitespace type tokens* // // These typically take the form `{{- expression -}}` or `{{- expression }}` or `{{ expression -}}`. { type: Twig.token.type.outputWhitespacePre, open: '{{-', close: '}}' }, { type: Twig.token.type.outputWhitespacePost, open: '{{', close: '-}}' }, { type: Twig.token.type.outputWhitespaceBoth, open: '{{-', close: '-}}' }, { type: Twig.token.type.logicWhitespacePre, open: '{%-', close: '%}' }, { type: Twig.token.type.logicWhitespacePost, open: '{%', close: '-%}' }, { type: Twig.token.type.logicWhitespaceBoth, open: '{%-', close: '-%}' }, // *Output type tokens* // // These typically take the form `{{ expression }}`. { type: Twig.token.type.output, open: '{{', close: '}}' }, // *Logic type tokens* // // These typically take a form like `{% if expression %}` or `{% endif %}` { type: Twig.token.type.logic, open: '{%', close: '%}' }, // *Comment type tokens* // // These take the form `{# anything #}` { type: Twig.token.type.comment, open: '{#', close: '#}' }]; /** * What characters start "strings" in token definitions. We need this to ignore token close * strings inside an expression. */ Twig.token.strings = ['"', '\'']; Twig.token.findStart = function (template) { var output = { position: null, def: null }; var closePosition = null; var len = Twig.token.definitions.length; var i; var tokenTemplate; var firstKeyPosition; var closeKeyPosition; for (i = 0; i < len; i++) { tokenTemplate = Twig.token.definitions[i]; firstKeyPosition = template.indexOf(tokenTemplate.open); closeKeyPosition = template.indexOf(tokenTemplate.close); Twig.log.trace('Twig.token.findStart: ', 'Searching for ', tokenTemplate.open, ' found at ', firstKeyPosition); // Special handling for mismatched tokens if (firstKeyPosition >= 0) { // This token matches the template if (tokenTemplate.open.length !== tokenTemplate.close.length) { // This token has mismatched closing and opening tags if (closeKeyPosition < 0) { // This token's closing tag does not match the template continue; } } } // Does this token occur before any other types? if (firstKeyPosition >= 0 && (output.position === null || firstKeyPosition < output.position)) { output.position = firstKeyPosition; output.def = tokenTemplate; closePosition = closeKeyPosition; } else if (firstKeyPosition >= 0 && output.position !== null && firstKeyPosition === output.position) { /* This token exactly matches another token, greedily match to check if this token has a greater specificity */ if (tokenTemplate.open.length > output.def.open.length) { // This token's opening tag is more specific than the previous match output.position = firstKeyPosition; output.def = tokenTemplate; closePosition = closeKeyPosition; } else if (tokenTemplate.open.length === output.def.open.length) { if (tokenTemplate.close.length > output.def.close.length) { // This token's opening tag is as specific as the previous match, // but the closing tag has greater specificity if (closeKeyPosition >= 0 && closeKeyPosition < closePosition) { // This token's closing tag exists in the template, // and it occurs sooner than the previous match output.position = firstKeyPosition; output.def = tokenTemplate; closePosition = closeKeyPosition; } } else if (closeKeyPosition >= 0 && closeKeyPosition < closePosition) { // This token's closing tag is not more specific than the previous match, // but it occurs sooner than the previous match output.position = firstKeyPosition; output.def = tokenTemplate; closePosition = closeKeyPosition; } } } } return output; }; Twig.token.findEnd = function (template, tokenDef, start) { var end = null; var found = false; var offset = 0; // String position variables var strPos = null; var strFound = null; var pos = null; var endOffset = null; var thisStrPos = null; var endStrPos = null; // For loop variables var i; var l; while (!found) { strPos = null; strFound = null; pos = template.indexOf(tokenDef.close, offset); if (pos >= 0) { end = pos; found = true; } else { // Throw an exception throw new Twig.Error('Unable to find closing bracket \'' + tokenDef.close + '\' opened near template position ' + start); } // Ignore quotes within comments; just look for the next comment close sequence, // regardless of what comes before it. https://github.com/justjohn/twig.js/issues/95 if (tokenDef.type === Twig.token.type.comment) { break; } // Ignore quotes within raw tag // Fixes #283 if (tokenDef.type === Twig.token.type.raw) { break; } l = Twig.token.strings.length; for (i = 0; i < l; i += 1) { thisStrPos = template.indexOf(Twig.token.strings[i], offset); if (thisStrPos > 0 && thisStrPos < pos && (strPos === null || thisStrPos < strPos)) { strPos = thisStrPos; strFound = Twig.token.strings[i]; } } // We found a string before the end of the token, now find the string's end and set the search offset to it if (strPos !== null) { endOffset = strPos + 1; end = null; found = false; for (;;) { endStrPos = template.indexOf(strFound, endOffset); if (endStrPos < 0) { throw Twig.Error('Unclosed string in template'); } // Ignore escaped quotes if (template.slice(endStrPos - 1, endStrPos) === '\\') { endOffset = endStrPos + 1; } else { offset = endStrPos + 1; break; } } } } return end; }; /** * Convert a template into high-level tokens. */ Twig.tokenize = function (template) { var tokens = []; // An offset for reporting errors locations and the position of the nodes in the template. var currentPosition = 0; // The start and type of the first token found in the template. var foundToken = null; // The end position of the matched token. var end = null; while (template.length > 0) { // Find the first occurance of any token type in the template foundToken = Twig.token.findStart(template); Twig.log.trace('Twig.tokenize: ', 'Found token: ', foundToken); if (foundToken.position === null) { // No more tokens -> add the rest of the template as a raw-type token tokens.push({ type: Twig.token.type.raw, value: template, position: { start: currentPosition, end: currentPosition + foundToken.position } }); template = ''; } else { // Add a raw type token for anything before the start of the token if (foundToken.position > 0) { tokens.push({ type: Twig.token.type.raw, value: template.slice(0, Math.max(0, foundToken.position)), position: { start: currentPosition, end: currentPosition + Math.max(0, foundToken.position) } }); } template = template.slice(foundToken.position + foundToken.def.open.length); currentPosition += foundToken.position + foundToken.def.open.length; // Find the end of the token end = Twig.token.findEnd(template, foundToken.def, currentPosition); Twig.log.trace('Twig.tokenize: ', 'Token ends at ', end); tokens.push({ type: foundToken.def.type, value: template.slice(0, Math.max(0, end)).trim(), position: { start: currentPosition - foundToken.def.open.length, end: currentPosition + end + foundToken.def.close.length } }); if (template.slice(end + foundToken.def.close.length, end + foundToken.def.close.length + 1) === '\n') { switch (foundToken.def.type) { case 'logic_whitespace_pre': case 'logic_whitespace_post': case 'logic_whitespace_both': case 'logic': // Newlines directly after logic tokens are ignored end += 1; break; default: break; } } template = template.slice(end + foundToken.def.close.length); // Increment the position in the template currentPosition += end + foundToken.def.close.length; } } return tokens; }; Twig.compile = function (tokens) { var self = this; try { // Output and intermediate stacks var output = []; var stack = []; // The tokens between open and close tags var intermediateOutput = []; var token = null; var logicToken = null; var unclosedToken = null; // Temporary previous token. var prevToken = null; // Temporary previous output. var prevOutput = null; // Temporary previous intermediate output. var prevIntermediateOutput = null; // The previous token's template var prevTemplate = null; // Token lookahead var nextToken = null; // The output token var tokOutput = null; // Logic Token values var type = null; var open = null; var next = null; var compileOutput = function compileOutput(token) { Twig.expression.compile.call(self, token); if (stack.length > 0) { intermediateOutput.push(token); } else { output.push(token); } }; var compileLogic = function compileLogic(token) { // Compile the logic token logicToken = Twig.logic.compile.call(self, token); logicToken.position = token.position; type = logicToken.type; open = Twig.logic.handler[type].open; next = Twig.logic.handler[type].next; Twig.log.trace('Twig.compile: ', 'Compiled logic token to ', logicToken, ' next is: ', next, ' open is : ', open); // Not a standalone token, check logic stack to see if this is expected if (open !== undefined && !open) { prevToken = stack.pop(); prevTemplate = Twig.logic.handler[prevToken.type]; if (!prevTemplate.next.includes(type)) { throw new Error(type + ' not expected after a ' + prevToken.type); } prevToken.output = prevToken.output || []; prevToken.output = prevToken.output.concat(intermediateOutput); intermediateOutput = []; tokOutput = { type: Twig.token.type.logic, token: prevToken, position: { open: prevToken.position, close: token.position } }; if (stack.length > 0) { intermediateOutput.push(tokOutput); } else { output.push(tokOutput); } } // This token requires additional tokens to complete the logic structure. if (next !== undefined && next.length > 0) { Twig.log.trace('Twig.compile: ', 'Pushing ', logicToken, ' to logic stack.'); if (stack.length > 0) { // Put any currently held output into the output list of the logic operator // currently at the head of the stack before we push a new one on. prevToken = stack.pop(); prevToken.output = prevToken.output || []; prevToken.output = prevToken.output.concat(intermediateOutput); stack.push(prevToken); intermediateOutput = []; } // Push the new logic token onto the logic stack stack.push(logicToken); } else if (open !== undefined && open) { tokOutput = { type: Twig.token.type.logic, token: logicToken, position: logicToken.position }; // Standalone token (like {% set ... %} if (stack.length > 0) { intermediateOutput.push(tokOutput); } else { output.push(tokOutput); } } }; while (tokens.length > 0) { token = tokens.shift(); prevOutput = output[output.length - 1]; prevIntermediateOutput = intermediateOutput[intermediateOutput.length - 1]; nextToken = tokens[0]; Twig.log.trace('Compiling token ', token); switch (token.type) { case Twig.token.type.raw: if (stack.length > 0) { intermediateOutput.push(token); } else { output.push(token); } break; case Twig.token.type.logic: compileLogic.call(self, token); break; // Do nothing, comments should be ignored case Twig.token.type.comment: break; case Twig.token.type.output: compileOutput.call(self, token); break; // Kill whitespace ahead and behind this token case Twig.token.type.logicWhitespacePre: case Twig.token.type.logicWhitespacePost: case Twig.token.type.logicWhitespaceBoth: case Twig.token.type.outputWhitespacePre: case Twig.token.type.outputWhitespacePost: case Twig.token.type.outputWhitespaceBoth: if (token.type !== Twig.token.type.outputWhitespacePost && token.type !== Twig.token.type.logicWhitespacePost) { if (prevOutput) { // If the previous output is raw, pop it off if (prevOutput.type === Twig.token.type.raw) { output.pop(); prevOutput.value = prevOutput.value.trimEnd(); // Repush the previous output output.push(prevOutput); } } if (prevIntermediateOutput) { // If the previous intermediate output is raw, pop it off if (prevIntermediateOutput.type === Twig.token.type.raw) { intermediateOutput.pop(); prevIntermediateOutput.value = prevIntermediateOutput.value.trimEnd(); // Repush the previous intermediate output intermediateOutput.push(prevIntermediateOutput); } } } // Compile this token switch (token.type) { case Twig.token.type.outputWhitespacePre: case Twig.token.type.outputWhitespacePost: case Twig.token.type.outputWhitespaceBoth: compileOutput.call(self, token); break; case Twig.token.type.logicWhitespacePre: case Twig.token.type.logicWhitespacePost: case Twig.token.type.logicWhitespaceBoth: compileLogic.call(self, token); break; default: break; } if (token.type !== Twig.token.type.outputWhitespacePre && token.type !== Twig.token.type.logicWhitespacePre) { if (nextToken) { // If the next token is raw, shift it out if (nextToken.type === Twig.token.type.raw) { tokens.shift(); nextToken.value = nextToken.value.trimStart(); // Unshift the next token tokens.unshift(nextToken); } } } break; default: break; } Twig.log.trace('Twig.compile: ', ' Output: ', output, ' Logic Stack: ', stack, ' Pending Output: ', intermediateOutput); } // Verify that there are no logic tokens left in the stack. if (stack.length > 0) { unclosedToken = stack.pop(); throw new Error('Unable to find an end tag for ' + unclosedToken.type + ', expecting one of ' + unclosedToken.next); } return output; } catch (error) { if (self.options.rethrow) { if (error.type === 'TwigException' && !error.file) { error.file = self.id; } throw error; } else { Twig.log.error('Error compiling twig template ' + self.id + ': '); if (error.stack) { Twig.log.error(error.stack); } else { Twig.log.error(error.toString()); } } } }; function handleException(state, ex) { if (state.template.options.rethrow) { if (typeof ex === 'string') { ex = new Twig.Error(ex); } if (ex.type === 'TwigException' && !ex.file) { ex.file = state.template.id; } throw ex; } else { Twig.log.error('Error parsing twig template ' + state.template.id + ': '); if (ex.stack) { Twig.log.error(ex.stack); } else { Twig.log.error(ex.toString()); } if (Twig.debug) { return ex.toString(); } } } /** * Tokenize and compile a string template. * * @param {string} data The template. * * @return {Array} The compiled tokens. */ Twig.prepare = function (data) { // Tokenize Twig.log.debug('Twig.prepare: ', 'Tokenizing ', data); var rawTokens = Twig.tokenize.call(this, data); // Compile Twig.log.debug('Twig.prepare: ', 'Compiling ', rawTokens); var tokens = Twig.compile.call(this, rawTokens); Twig.log.debug('Twig.prepare: ', 'Compiled ', tokens); return tokens; }; /** * Join the output token's stack and escape it if needed * * @param {Array} Output token's stack * * @return {string|String} Autoescaped output */ Twig.output = function (output) { var autoescape = this.options.autoescape; if (!autoescape) { return output.join(''); } var strategy = typeof autoescape === 'string' ? autoescape : 'html'; var escapedOutput = output.map(function (str) { if (str && str.twigMarkup !== true && str.twigMarkup !== strategy && !(strategy === 'html' && str.twigMarkup === 'html_attr')) { str = Twig.filters.escape(str, [strategy]); } return str; }); if (escapedOutput.length === 0) { return ''; } var joinedOutput = escapedOutput.join(''); if (joinedOutput.length === 0) { return ''; } return new Twig.Markup(joinedOutput, true); }; // Namespace for template storage and retrieval Twig.Templates = { /** * Registered template loaders - use Twig.Templates.registerLoader to add supported loaders * @type {Object} */ loaders: {}, /** * Registered template parsers - use Twig.Templates.registerParser to add supported parsers * @type {Object} */ parsers: {}, /** * Cached / loaded templates * @type {Object} */ registry: {} }; /** * Is this id valid for a twig template? * * @param {string} id The ID to check. * * @throws {Twig.Error} If the ID is invalid or used. * @return {boolean} True if the ID is valid. */ Twig.validateId = function (id) { if (id === 'prototype') { throw new Twig.Error(id + ' is not a valid twig identifier'); } else if (Twig.cache && Object.hasOwnProperty.call(Twig.Templates.registry, id)) { throw new Twig.Error('There is already a template with the ID ' + id); } return true; }; /** * Register a template loader * * @example * Twig.extend(function (Twig) { * Twig.Templates.registerLoader('custom_loader', function (location, params, callback, errorCallback) { * // ... load the template ... * params.data = loadedTemplateData; * // create and return the template * var template = new Twig.Template(params); * if (typeof callback === 'function') { * callback(template); * } * return template; * }); * }); * * @param {String} methodName The method this loader is intended for (ajax, fs) * @param {Function} func The function to execute when loading the template * @param {Object|undefined} scope Optional scope parameter to bind func to * * @throws Twig.Error * * @return {void} */ Twig.Templates.registerLoader = function (methodName, func, scope) { if (typeof func !== 'function') { throw new Twig.Error('Unable to add loader for ' + methodName + ': Invalid function reference given.'); } if (scope) { func = func.bind(scope); } this.loaders[methodName] = func; }; /** * Remove a registered loader * * @param {String} methodName The method name for the loader you wish to remove * * @return {void} */ Twig.Templates.unRegisterLoader = function (methodName) { if (this.isRegisteredLoader(methodName)) { delete this.loaders[methodName]; } }; /** * See if a loader is registered by its method name * * @param {String} methodName The name of the loader you are looking for * * @return {boolean} */ Twig.Templates.isRegisteredLoader = function (methodName) { return Object.hasOwnProperty.call(this.loaders, methodName); }; /** * Register a template parser * * @example * Twig.extend(function (Twig) { * Twig.Templates.registerParser('custom_parser', function (params) { * // this template source can be accessed in params.data * var template = params.data * * // ... custom process that modifies the template * * // return the parsed template * return template; * }); * }); * * @param {String} methodName The method this parser is intended for (twig, source) * @param {Function} func The function to execute when parsing the template * @param {Object|undefined} scope Optional scope parameter to bind func to * * @throws Twig.Error * * @return {void} */ Twig.Templates.registerParser = function (methodName, func, scope) { if (typeof func !== 'function') { throw new Twig.Error('Unable to add parser for ' + methodName + ': Invalid function regerence given.'); } if (scope) { func = func.bind(scope); } this.parsers[methodName] = func; }; /** * Remove a registered parser * * @param {String} methodName The method name for the parser you wish to remove * * @return {void} */ Twig.Templates.unRegisterParser = function (methodName) { if (this.isRegisteredParser(methodName)) { delete this.parsers[methodName]; } }; /** * See if a parser is registered by its method name * * @param {String} methodName The name of the parser you are looking for * * @return {boolean} */ Twig.Templates.isRegisteredParser = function (methodName) { return Object.hasOwnProperty.call(this.parsers, methodName); }; /** * Save a template object to the store. * * @param {Twig.Template} template The twig.js template to store. */ Twig.Templates.save = function (template) { if (template.id === undefined) { throw new Twig.Error('Unable to save template with no id'); } Twig.Templates.registry[template.id] = template; }; /** * Load a previously saved template from the store. * * @param {string} id The ID of the template to load. * * @return {Twig.Template} A twig.js template stored with the provided ID. */ Twig.Templates.load = function (id) { if (!Object.hasOwnProperty.call(Twig.Templates.registry, id)) { return null; } return Twig.Templates.registry[id]; }; /** * Load a template from a remote location using AJAX and saves in with the given ID. * * Available parameters: * * async: Should the HTTP request be performed asynchronously. * Defaults to true. * method: What method should be used to load the template * (fs or ajax) * parser: What method should be used to parse the template * (twig or source) * precompiled: Has the template already been compiled. * * @param {string} location The remote URL to load as a template. * @param {Object} params The template parameters. * @param {function} callback A callback triggered when the template finishes loading. * @param {function} errorCallback A callback triggered if an error occurs loading the template. * * */ Twig.Templates.loadRemote = function (location, params, callback, errorCallback) { // Default to the URL so the template is cached. var id = typeof params.id === 'undefined' ? location : params.id; var cached = Twig.Templates.registry[id]; // Check for existing template if (Twig.cache && typeof cached !== 'undefined') { // A template is already saved with the given id. if (typeof callback === 'function') { callback(cached); } // TODO: if async, return deferred promise return cached; } // If the parser name hasn't been set, default it to twig params.parser = params.parser || 'twig'; params.id = id; // Default to async if (typeof params.async === 'undefined') { params.async = true; } // Assume 'fs' if the loader is not defined var loader = this.loaders[params.method] || this.loaders.fs; return loader.call(this, location, params, callback, errorCallback); }; // Determine object type function is(type, obj) { var clas = Object.prototype.toString.call(obj).slice(8, -1); return obj !== undefined && obj !== null && clas === type; } /** * A wrapper for template blocks. * * @param {Twig.Template} The template that the block was originally defined in. * @param {Object} The compiled block token. */ Twig.Block = function (template, token) { this.template = template; this.token = token; }; /** * Render the block using a specific parse state and context. * * @param {Twig.ParseState} parseState * @param {Object} context * * @return {Promise} */ Twig.Block.prototype.render = function (parseState, context) { var originalTemplate = parseState.template; var promise; parseState.template = this.template; if (this.token.expression) { promise = Twig.expression.parseAsync.call(parseState, this.token.output, context); } else { promise = parseState.parseAsync(this.token.output, context); } return promise.then(function (value) { return Twig.expression.parseAsync.call(parseState, { type: Twig.expression.type.string, value: value }, context); }).then(function (output) { parseState.template = originalTemplate; return output; }); }; /** * Holds the state needed to parse a template. * * @param {Twig.Template} template The template that the tokens being parsed are associated with. * @param {Object} blockOverrides Any blocks that should override those defined in the associated template. */ Twig.ParseState = function (template, blockOverrides, context) { this.renderedBlocks = {}; this.overrideBlocks = blockOverrides === undefined ? {} : blockOverrides; this.context = context === undefined ? {} : context; this.macros = {}; this.nestingStack = []; this.template = template; }; /** * Get a block by its name, resolving in the following order: * - override blocks specified when initialized (except when excluded) * - blocks resolved from the associated template * - blocks resolved from the parent template when extending * * @param {String} name The name of the block to return. * @param {Boolean} checkOnlyInheritedBlocks Whether to skip checking the overrides and associated template, will not skip by default. * * @return {Twig.Block|undefined} */ Twig.ParseState.prototype.getBlock = function (name, checkOnlyInheritedBlocks) { var block; if (checkOnlyInheritedBlocks !== true) { // Blocks specified when initialized block = this.overrideBlocks[name]; } if (block === undefined) { // Block defined by the associated template block = this.template.getBlock(name, checkOnlyInheritedBlocks); } if (block === undefined && this.template.parentTemplate !== null) { // Block defined in the parent template when extending block = this.template.parentTemplate.getBlock(name); } return block; }; /** * Get all the available blocks, resolving in the following order: * - override blocks specified when initialized * - blocks resolved from the associated template * - blocks resolved from the parent template when extending (except when excluded) * * @param {Boolean} includeParentBlocks Whether to get blocks from the parent template when extending, will always do so by default. * * @return {Object} */ Twig.ParseState.prototype.getBlocks = function (includeParentBlocks) { var blocks = {}; if (includeParentBlocks !== false && this.template.parentTemplate !== null && // Prevent infinite loop this.template.parentTemplate !== this.template) { // Blocks from the parent template when extending blocks = this.template.parentTemplate.getBlocks(); } blocks = _objectSpread(_objectSpread(_objectSpread({}, blocks), this.template.getBlocks()), this.overrideBlocks); return blocks; }; /** * Get the closest token of a specific type to the current nest level. * * @param {String} type The logic token type * * @return {Object} */ Twig.ParseState.prototype.getNestingStackToken = function (type) { var matchingToken; this.nestingStack.forEach(function (token) { if (matchingToken === undefined && token.type === type) { matchingToken = token; } }); return matchingToken; }; /** * Parse a set of tokens using the current state. * * @param {Array} tokens The compiled tokens. * @param {Object} context The context to set the state to while parsing. * @param {Boolean} allowAsync Whether to parse asynchronously. * @param {Object} blocks Blocks that should override any defined while parsing. * * @return {String} The rendered tokens. * */ Twig.ParseState.prototype.parse = function (tokens, context, allowAsync) { var state = this; var output = []; // Store any error that might be thrown by the promise chain. var err = null; // This will be set to isAsync if template renders synchronously var isAsync = true; var promise = null; // Track logic chains var chain = true; if (context) { state.context = context; } /* * Extracted into it's own function such that the function * does not get recreated over and over again in the `forEach` * loop below. This method can be compiled and optimized * a single time instead of being recreated on each iteration. */ function outputPush(o) { output.push(o); } function parseTokenLogic(logic) { if (typeof logic.chain !== 'undefined') { chain = logic.chain; } if (typeof logic.context !== 'undefined') { state.context = logic.context; } if (typeof logic.output !== 'undefined') { output.push(logic.output); } } promise = Twig.async.forEach(tokens, function (token) { Twig.log.debug('Twig.ParseState.parse: ', 'Parsing token: ', token); switch (token.type) { case Twig.token.type.raw: output.push(Twig.filters.raw(token.value)); break; case Twig.token.type.logic: return Twig.logic.parseAsync.call(state, token.token /* logicToken */, state.context, chain).then(parseTokenLogic); case Twig.token.type.comment: // Do nothing, comments should be ignored break; // Fall through whitespace to output case Twig.token.type.outputWhitespacePre: case Twig.token.type.outputWhitespacePost: case Twig.token.type.outputWhitespaceBoth: case Twig.token.type.output: Twig.log.debug('Twig.ParseState.parse: ', 'Output token: ', token.stack); // Parse the given expression in the given context return Twig.expression.parseAsync.call(state, token.stack, state.context).then(outputPush); default: break; } }).then(function () { output = Twig.output.call(state.template, output); isAsync = false; return output; })["catch"](function (error) { if (allowAsync) { handleException(state, error); } err = error; }); // If `allowAsync` we will always return a promise since we do not // know in advance if we are going to run asynchronously or not. if (allowAsync) { return promise; } // Handle errors here if we fail synchronously. if (err !== null) { return handleException(state, err); } // If `allowAsync` is not true we should not allow the user // to use asynchronous functions or filters. if (isAsync) { throw new Twig.Error('You are using Twig.js in sync mode in combination with async extensions.'); } return output; }; /** * Create a new twig.js template. * * Parameters: { * data: The template, either pre-compiled tokens or a string template * id: The name of this template * } * * @param {Object} params The template parameters. */ Twig.Template = function (params) { var data = params.data, id = params.id, base = params.base, path = params.path, url = params.url, name = params.name, method = params.method, options = params.options; // # What is stored in a Twig.Template // // The Twig Template hold several chucks of data. // // { // id: The token ID (if any) // tokens: The list of tokens that makes up this template. // base: The base template (if any) // options: { // Compiler/parser