UNPKG

twig

Version:

JS port of the Twig templating language.

1,440 lines (1,248 loc) 50.8 kB
// ## 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(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 () { const output = this.name + ': ' + this.message; return output; }; /** * Wrapper for logging to the console. */ Twig.log = { trace(...args) { if (Twig.trace && console) { console.log(Array.prototype.slice.call(args)); } }, debug(...args) { if (Twig.debug && console) { 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 (...args) { console.error(...args); }; } else if (typeof console.log !== 'undefined') { Twig.log.error = function (...args) { console.log(...args); }; } /** * 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) { const output = { position: null, def: null }; let closePosition = null; const len = Twig.token.definitions.length; let i; let tokenTemplate; let firstKeyPosition; let 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) { let end = null; let found = false; let offset = 0; // String position variables let strPos = null; let strFound = null; let pos = null; let endOffset = null; let thisStrPos = null; let endStrPos = null; // For loop variables let i; let 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) { const tokens = []; // An offset for reporting errors locations and the position of the nodes in the template. let currentPosition = 0; // The start and type of the first token found in the template. let foundToken = null; // The end position of the matched token. let 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) { const self = this; try { // Output and intermediate stacks const output = []; const stack = []; // The tokens between open and close tags let intermediateOutput = []; let token = null; let logicToken = null; let unclosedToken = null; // Temporary previous token. let prevToken = null; // Temporary previous output. let prevOutput = null; // Temporary previous intermediate output. let prevIntermediateOutput = null; // The previous token's template let prevTemplate = null; // Token lookahead let nextToken = null; // The output token let tokOutput = null; // Logic Token values let type = null; let open = null; let next = null; const compileOutput = function (token) { Twig.expression.compile.call(self, token); if (stack.length > 0) { intermediateOutput.push(token); } else { output.push(token); } }; const compileLogic = function (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); const rawTokens = Twig.tokenize.call(this, data); // Compile Twig.log.debug('Twig.prepare: ', 'Compiling ', rawTokens); const 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) { const {autoescape} = this.options; if (!autoescape) { return output.join(''); } const strategy = (typeof autoescape === 'string') ? autoescape : 'html'; const escapedOutput = output.map(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 ''; } const 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. const id = typeof params.id === 'undefined' ? location : params.id; const 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 const loader = this.loaders[params.method] || this.loaders.fs; return loader.call(this, location, params, callback, errorCallback); }; // Determine object type function is(type, obj) { const 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) { const originalTemplate = parseState.template; let 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(value => { return Twig.expression.parseAsync.call( parseState, { type: Twig.expression.type.string, value }, context ); }) .then(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) { let 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) { let 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 = { ...blocks, // Override with any blocks defined within the associated template ...this.template.getBlocks(), // Override with any blocks specified when initialized ...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) { let matchingToken; this.nestingStack.forEach(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) { const state = this; let output = []; // Store any error that might be thrown by the promise chain. let err = null; // This will be set to isAsync if template renders synchronously let isAsync = true; let promise = null; // Track logic chains let 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, 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(() => { output = Twig.output.call(state.template, output); isAsync = false; return output; }).catch(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) { const {data, id, base, path, url, name, method, options} = params; // # 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 options // // strict_variables: true/false // Should missing variable/keys emit an error message. If false, they default to null. // } // } // this.base = base; this.blocks = { defined: {}, imported: {} }; this.id = id; this.method = method; this.name = name; this.options = options; this.parentTemplate = null; this.path = path; this.url = url; if (is('String', data)) { this.tokens = Twig.prepare.call(this, data); } else { this.tokens = data; } if (id !== undefined) { Twig.Templates.save(this); } }; /** * Get a block by its name, resolving in the following order: * - blocks defined in the template itself * - blocks imported from another template * * @param {String} name The name of the block to return. * @param {Boolean} checkOnlyInheritedBlocks Whether to skip checking the blocks defined in the template itself, will not skip by default. * * @return {Twig.Block|undefined} */ Twig.Template.prototype.getBlock = function (name, checkOnlyInheritedBlocks, checkImports = true) { let block; if (checkOnlyInheritedBlocks !== true) { block = this.blocks.defined[name]; } if (checkImports && block === undefined) { block = this.blocks.imported[name]; } if (block === undefined && this.parentTemplate !== null) { /** * Block defined in the parent template when extending. * This recursion is useful to inherit from ascendants. * But take care of not considering ascendants' {% use %} */ block = this.parentTemplate.getBlock(name, checkOnlyInheritedBlocks, checkImports = false); } return block; }; /** * Get all the available blocks, resolving in the following order: * - blocks defined in the template itself * - blocks imported from other templates * * @return {Object} */ Twig.Template.prototype.getBlocks = function () { let blocks = {}; blocks = { ...blocks, // Get any blocks imported from other templates ...this.blocks.imported, // Override with any blocks defined within the template itself ...this.blocks.defined }; return blocks; }; Twig.Template.prototype.render = function (context, params, allowAsync) { const template = this; params = params || {}; return Twig.async.potentiallyAsync(template, allowAsync, () => { const state = new Twig.ParseState(template, params.blocks, context); return state.parseAsync(template.tokens) .then(output => { let parentTemplate; let url; if (template.parentTemplate !== null) { // This template extends another template if (template.options.allowInlineIncludes) { // The template is provided inline parentTemplate = Twig.Templates.load(template.parentTemplate); if (parentTemplate) { parentTemplate.options = template.options; } } // Check for the template file via include if (!parentTemplate) { url = Twig.path.parsePath(template, template.parentTemplate); parentTemplate = Twig.Templates.loadRemote(url, { method: template.getLoaderMethod(), base: template.base, async: false, id: url, options: template.options }); } template.parentTemplate = parentTemplate; return template.parentTemplate.renderAsync( state.context, { blocks: state.getBlocks(false), isInclude: true } ); } if (params.isInclude === true) { return output; } return output.valueOf(); }); }); }; Twig.Template.prototype.importFile = function (file) { let url = null; let subTemplate; if (!this.url && this.options.allowInlineIncludes) { file = this.path ? Twig.path.parsePath(this, file) : file; subTemplate = Twig.Templates.load(file); if (!subTemplate) { subTemplate = Twig.Templates.loadRemote(url, { id: file, method: this.getLoaderMethod(), async: false, path: file, options: this.options }); if (!subTemplate) { throw new Twig.Error('Unable to find the template ' + file); } } subTemplate.options = this.options; return subTemplate; } url = Twig.path.parsePath(this, file); // Load blocks from an external file subTemplate = Twig.Templates.loadRemote(url, { method: this.getLoaderMethod(), base: this.base, async: false, options: this.options, id: url }); return subTemplate; }; Twig.Template.prototype.getLoaderMethod = function () { if (this.path) {