UNPKG

rbc-twig-render

Version:
1,563 lines (1,358 loc) 119 kB
/** * RBC twigjs render * * @copyright 2011-2016 John Roepke and the Twig.js Contributors * @license Available under the BSD 2-Clause License * @link https://github.com/twigjs/twig.js */ var Twig = { VERSION: '0.0.16', _getType: function(obj) { return Object.prototype.toString.call(obj).slice(8, -1); }, _is: function (type, obj) { var objType = Twig._getType(obj); if (type === 'Number' && objType === 'Number') { return !isNaN(obj); } else { return objType === type; } } }; // ## twig.core.js // // This file handles template level tokenizing, compiling and parsing. (function (Twig) { "use strict"; Twig.trace = false; Twig.debug = false; Twig.placeholders = { parent: "{{|PARENT|}}" }; Twig.forEach = function (arr, callback, thisArg) { if (Array.prototype.forEach) { return arr.forEach(callback, thisArg); } var T, k; if (arr == null) { throw new TypeError(" this is null or not defined"); } // 1. Let O be the result of calling ToObject passing the |this| value as the argument. var O = Object(arr); // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length". // 3. Let len be ToUint32(lenValue). var len = O.length >>> 0; // Hack to convert O.length to a UInt32 // 4. If IsCallable(callback) is false, throw a TypeError exception. // See: http://es5.github.com/#x9.11 if ({}.toString.call(callback) != "[object Function]") { throw new TypeError(callback + " is not a function"); } // 5. If thisArg was supplied, let T be thisArg; else let T be undefined. if (thisArg) { T = thisArg; } // 6. Let k be 0 k = 0; // 7. Repeat, while k < len while (k < len) { var kValue; // a. Let Pk be ToString(k). // This is implicit for LHS operands of the in operator // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk. // This step can be combined with c // c. If kPresent is true, then if (k in O) { // i. Let kValue be the result of calling the Get internal method of O with argument Pk. kValue = O[k]; // ii. Call the Call internal method of callback with T as the this value and // argument list containing kValue, k, and O. callback.call(T, kValue, k, O); } // d. Increase k by 1. k++; } // 8. return undefined }; Twig.merge = function (target, source, onlyChanged) { Twig.forEach(Object.keys(source), function (key) { if (onlyChanged && !(key in target)) { return; } target[key] = source[key] }); return target; }; /** * Exception thrown by twig.js. */ Twig.Error = function (message) { this.message = message; this.name = "TwigException"; this.type = "TwigException"; }; /** * 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 () { if (Twig.trace && console) { console.log(Array.prototype.slice.call(arguments)); } }, debug: function () { if (Twig.debug && console) { console.log(Array.prototype.slice.call(arguments)); } } }; if (typeof console !== "undefined") { if (typeof console.error !== "undefined") { Twig.log.error = function() { console.error.apply(console, arguments); } } else if (typeof console.log !== "undefined") { Twig.log.error = function() { console.log.apply(console, arguments); } } } else { Twig.log.error = function() {}; } /** * Wrapper for child context objects in Twig. * * @param {Object} context Values to initialize the context with. */ Twig.ChildContext = function(context) { var ChildContext = function ChildContext() {}; ChildContext.prototype = context; return new ChildContext(); }; /** * 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', output_whitespace_pre: 'output_whitespace_pre', output_whitespace_post: 'output_whitespace_post', output_whitespace_both: 'output_whitespace_both', logic_whitespace_pre: 'logic_whitespace_pre', logic_whitespace_post: 'logic_whitespace_post', logic_whitespace_both: 'logic_whitespace_both' }; /** * What characters start "strings" in token definitions. We need this to ignore token close * strings inside an expression. */ Twig.token.strings = ['"', "'"]; /** * Parse a compiled template. * * @param {Array} tokens The compiled tokens. * @param {Object} context The render context. * * @return {string} The parsed template. */ Twig.parse = function (tokens, context) { try { var output = [], // Track logic chains chain = true, that = this; Twig.forEach(tokens, function parseToken(token) { Twig.log.debug("Twig.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: var logic_token = token.token, logic = Twig.logic.parse.call(that, logic_token, context, chain); if (logic.chain !== undefined) { chain = logic.chain; } if (logic.context !== undefined) { context = logic.context; } if (logic.output !== undefined) { output.push(logic.output); } break; case Twig.token.type.comment: // Do nothing, comments should be ignored break; //Fall through whitespace to output case Twig.token.type.output_whitespace_pre: case Twig.token.type.output_whitespace_post: case Twig.token.type.output_whitespace_both: case Twig.token.type.output: Twig.log.debug("Twig.parse: ", "Output token: ", token.stack); // Parse the given expression in the given context output.push(Twig.expression.parse.call(that, token.stack, context)); break; } }); return Twig.output.call(this, output); } catch (ex) { if (this.options.rethrow) { throw ex; } else { Twig.log.error("Error parsing twig template " + this.id + ": "); if (ex.stack) { Twig.log.error(ex.stack); } else { Twig.log.error(ex.toString()); } if (Twig.debug) { return ex.toString(); } } } }; /** * 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) { if (!this.options.autoescape) { return output.join(""); } var strategy = 'html'; if (typeof this.options.autoescape === 'string') strategy = this.options.autoescape; // [].map would be better but it's not supported by IE8- var escaped_output = []; Twig.forEach(output, function (str) { if (str && (str.twig_markup !== true && str.twig_markup !== strategy)) { str = Twig.filters.escape(str, [strategy]); } escaped_output.push(str); }); return Twig.Markup(escaped_output.join("")); } // Namespace for template storage and retrieval Twig.Templates = { /** * Registered template parsers - use Twig.Templates.registerParser to add supported parsers * @type {Object} */ parsers: {}, /** * Cached / loaded templates * @type {Object} */ registry: {} }; /** * 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} method_name 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(method_name, func, scope) { if (typeof func !== 'function') { throw new Twig.Error('Unable to add parser for ' + method_name + ': Invalid function regerence given.'); } if (scope) { func = func.bind(scope); } this.parsers[method_name] = func; }; /** * Remove a registered parser * * @param {String} method_name The method name for the parser you wish to remove * * @return {void} */ Twig.Templates.unRegisterParser = function(method_name) { if (this.isRegisteredParser(method_name)) { delete this.parsers[method_name]; } }; /** * See if a parser is registered by its method name * * @param {String} method_name The name of the parser you are looking for * * @return {boolean} */ Twig.Templates.isRegisteredParser = function(method_name) { return this.parsers.hasOwnProperty(method_name); }; /** * 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 (!Twig.Templates.registry.hasOwnProperty(id)) { return null; } return Twig.Templates.registry[id]; }; /** * Create a new twig.js template. * * Parameters: { * data: The template, either pre-compiled tokens or a string template * id: The name of this template * blocks: Any pre-existing block from a child template * } * * @param {Object} params The template parameters. */ Twig.Template = function(params) { var data = params.data, id = params.id, blocks = params.blocks, path = params.path, name = params.name, // parser options 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. // blocks: The list of block this template contains. // 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.id = id; this.path = path; this.name = name; this.options = options; this.reset(blocks); if (Twig._is('String', data)) { this.tokens = Twig.prepare.call(this, data); } else { this.tokens = data; } if (id !== undefined) { Twig.Templates.save(this); } }; Twig.Template.prototype.reset = function (blocks) { Twig.log.debug("Twig.Template.reset", "Reseting template " + this.id); this.blocks = {}; this.importedBlocks = []; this.originalBlockTokens = {}; this.child = { blocks: blocks || {} }; this.extend = null; }; Twig.Template.prototype.render = function (context, params) { params = params || {}; this.context = context || {}; // Clear any previous state this.reset(); if (params.blocks) { this.blocks = params.blocks; } var output = Twig.parse.call(this, this.tokens, this.context); // Does this template extend another if (this.extend) { var ext_template = Twig.Templates.load(this.extend); if (ext_template) { ext_template.options = this.options; } this.parent = ext_template; return this.parent.render(this.context, { blocks: this.blocks }); } if (params.output == 'blocks') { return this.blocks; } else { return output; } }; Twig.Template.prototype.importFile = function (file) { file = this.path ? this.path + '/' + file : file; var sub_template = Twig.Templates.load(file); if (!sub_template) { throw new Twig.Error("Unable to find the template " + file); } sub_template.options = this.options; return sub_template; }; Twig.Template.prototype.importBlocks = function (file, override) { var sub_template = this.importFile(file), context = this.context, that = this; override = override || false; sub_template.render(context); // Mixin blocks Twig.forEach(Object.keys(sub_template.blocks), function(key) { if (override || that.blocks[key] === undefined) { that.blocks[key] = sub_template.blocks[key]; that.importedBlocks.push(key); } }); }; /** * Create safe output * @param {string} Content safe to output * @return {String} Content wrapped into a String */ Twig.Markup = function(content, strategy) { if (typeof strategy == 'undefined') { strategy = true; } if (typeof content === 'string' && content.length > 0) { content = new String(content); content.twig_markup = strategy; } return content; }; return Twig; })(Twig); // ## twig.logic.js // // This file handles tokenizing, compiling and parsing logic tokens. {% ... %} (function (Twig) { "use strict"; /** * Namespace for logic handling. */ Twig.logic = {}; /** * Logic token types. */ Twig.logic.type = { if_: 'Twig.logic.type.if', endif: 'Twig.logic.type.endif', for_: 'Twig.logic.type.for', endfor: 'Twig.logic.type.endfor', else_: 'Twig.logic.type.else', elseif: 'Twig.logic.type.elseif', set: 'Twig.logic.type.set', setcapture:'Twig.logic.type.setcapture', endset: 'Twig.logic.type.endset', filter: 'Twig.logic.type.filter', endfilter: 'Twig.logic.type.endfilter', shortblock: 'Twig.logic.type.shortblock', block: 'Twig.logic.type.block', endblock: 'Twig.logic.type.endblock', extends_: 'Twig.logic.type.extends', use: 'Twig.logic.type.use', include: 'Twig.logic.type.include', spaceless: 'Twig.logic.type.spaceless', endspaceless: 'Twig.logic.type.endspaceless', macro: 'Twig.logic.type.macro', endmacro: 'Twig.logic.type.endmacro', import_: 'Twig.logic.type.import', from: 'Twig.logic.type.from', embed: 'Twig.logic.type.embed', endembed: 'Twig.logic.type.endembed' }; // Regular expressions for handling logic tokens. // // Properties: // // type: The type of expression this matches // // regex: A regular expression that matches the format of the token // // next: What logic tokens (if any) pop this token off the logic stack. If empty, the // logic token is assumed to not require an end tag and isn't push onto the stack. // // open: Does this tag open a logic expression or is it standalone. For example, // {% endif %} cannot exist without an opening {% if ... %} tag, so open = false. // // Functions: // // compile: A function that handles compiling the token into an output token ready for // parsing with the parse function. // // parse: A function that parses the compiled token into output (HTML / whatever the // template represents). Twig.logic.definitions = [ { /** * If type logic tokens. * * Format: {% if expression %} */ type: Twig.logic.type.if_, parse: function (token, context, chain) { var output = '', // Parse the expression result = Twig.expression.parse.call(this, token.stack, context); // Start a new logic chain chain = true; if (Twig.lib.boolval(result)) { chain = false; // parse if output output = Twig.parse.call(this, token.output, context); } return { chain: chain, output: output }; } }, { /** * Else if type logic tokens. * * Format: {% elseif expression %} */ type: Twig.logic.type.elseif, parse: function (token, context, chain) { var output = '', result = Twig.expression.parse.call(this, token.stack, context); if (chain && Twig.lib.boolval(result)) { chain = false; // parse if output output = Twig.parse.call(this, token.output, context); } return { chain: chain, output: output }; } }, { /** * Else if type logic tokens. * * Format: {% elseif expression %} */ type: Twig.logic.type.else_, parse: function (token, context, chain) { var output = ''; if (chain) { output = Twig.parse.call(this, token.output, context); } return { chain: chain, output: output }; } }, { /** * End if type logic tokens. * * Format: {% endif %} */ type: Twig.logic.type.endif }, { /** * For type logic tokens. * * Format: {% for expression %} */ type: Twig.logic.type.for_, parse: function (token, context, continue_chain) { // Parse expression var result = Twig.expression.parse.call(this, token.expression, context), output = [], len, index = 0, keyset, that = this, conditional = token.conditional, buildLoop = function (index, len) { var isConditional = conditional !== undefined; return { index: index + 1, index0: index, revindex: isConditional ? undefined : len - index, revindex0: isConditional ? undefined : len - index - 1, first: (index === 0), last: isConditional ? undefined : (index === len - 1), length: isConditional ? undefined : len, parent: context }; }, // run once for each iteration of the loop loop = function (key, value) { var inner_context = Twig.ChildContext(context); inner_context[token.value_var] = value; if (token.key_var) { inner_context[token.key_var] = key; } // Loop object inner_context.loop = buildLoop(index, len); if (conditional === undefined || Twig.expression.parse.call(that, conditional, inner_context)) { output.push(Twig.parse.call(that, token.output, inner_context)); index += 1; } // Delete loop-related variables from the context delete inner_context['loop']; delete inner_context[token.value_var]; delete inner_context[token.key_var]; // Merge in values that exist in context but have changed // in inner_context. Twig.merge(context, inner_context, true); }; if (Twig._is('Array', result)) { len = result.length; Twig.forEach(result, function (value) { var key = index; loop(key, value); }); } else if (Twig._is('Object', result)) { if (result._keys !== undefined) { keyset = result._keys; } else { keyset = Object.keys(result); } len = keyset.length; Twig.forEach(keyset, function (key) { // Ignore the _keys property, it's internal to twig.js if (key === "_keys") return; loop(key, result[key]); }); } // Only allow else statements if no output was generated continue_chain = (output.length === 0); return { chain: continue_chain, output: Twig.output.call(this, output) }; } }, { /** * End if type logic tokens. * * Format: {% endif %} */ type: Twig.logic.type.endfor, open: false }, { /** * Set type logic tokens. * * Format: {% set key = expression %} */ type: Twig.logic.type.set, parse: function (token, context, continue_chain) { var value = Twig.expression.parse.call(this, token.expression, context), key = token.key; if (value === context) { /* If storing the context in a variable, it needs to be a clone of the current state of context. Otherwise we have a context with infinite recursion. Fixes #341 */ value = Twig.lib.copy(value); } context[key] = value; return { chain: continue_chain, context: context }; } }, { /** * Set capture type logic tokens. * * Format: {% set key %} */ type: Twig.logic.type.setcapture, parse: function (token, context, continue_chain) { var value = Twig.parse.call(this, token.output, context), key = token.key; // set on both the global and local context this.context[key] = value; context[key] = value; return { chain: continue_chain, context: context }; } }, { /** * End set type block logic tokens. * * Format: {% endset %} */ type: Twig.logic.type.endset }, { /** * Filter logic tokens. * * Format: {% filter upper %} or {% filter lower|escape %} */ type: Twig.logic.type.filter, parse: function (token, context, chain) { var unfiltered = Twig.parse.call(this, token.output, context), stack = [{ type: Twig.expression.type.string, value: unfiltered }].concat(token.stack); var output = Twig.expression.parse.call(this, stack, context); return { chain: chain, output: output }; } }, { /** * End filter logic tokens. * * Format: {% endfilter %} */ type: Twig.logic.type.endfilter }, { /** * Block logic tokens. * * Format: {% block title %} */ type: Twig.logic.type.block, parse: function (token, context, chain) { var block_output, output, isImported = this.importedBlocks.indexOf(token.block) > -1, hasParent = this.blocks[token.block] && this.blocks[token.block].indexOf(Twig.placeholders.parent) > -1; // Don't override previous blocks unless they're imported with "use" // Loops should be exempted as well. if (this.blocks[token.block] === undefined || isImported || hasParent || context.loop || token.overwrite) { if (token.expression) { // Short blocks have output as an expression on the open tag (no body) block_output = Twig.expression.parse.call(this, { type: Twig.expression.type.string, value: Twig.expression.parse.call(this, token.output, context) }, context); } else { block_output = Twig.expression.parse.call(this, { type: Twig.expression.type.string, value: Twig.parse.call(this, token.output, context) }, context); } if (isImported) { // once the block is overridden, remove it from the list of imported blocks this.importedBlocks.splice(this.importedBlocks.indexOf(token.block), 1); } if (hasParent) { this.blocks[token.block] = Twig.Markup(this.blocks[token.block].replace(Twig.placeholders.parent, block_output)); } else { this.blocks[token.block] = block_output; } this.originalBlockTokens[token.block] = { type: token.type, block: token.block, output: token.output, overwrite: true }; } // Check if a child block has been set from a template extending this one. if (this.child.blocks[token.block]) { output = this.child.blocks[token.block]; } else { output = this.blocks[token.block]; } return { chain: chain, output: output }; } }, { /** * Block shorthand logic tokens. * * Format: {% block title expression %} */ type: Twig.logic.type.shortblock, parse: function (token, context, chain) { return Twig.logic.handler[Twig.logic.type.block].parse.apply(this, arguments); } }, { /** * End block logic tokens. * * Format: {% endblock %} */ type: Twig.logic.type.endblock }, { /** * Block logic tokens. * * Format: {% extends "template.twig" %} */ type: Twig.logic.type.extends_, parse: function (token, context, chain) { var template, innerContext = Twig.ChildContext(context); // Resolve filename var file = Twig.expression.parse.call(this, token.stack, context); // Set parent template this.extend = file; if (file instanceof Twig.Template) { template = file; } else { // Import file template = this.importFile(file); } // Render the template in case it puts anything in its context template.render(innerContext); // Extend the parent context with the extended context Twig.lib.extend(context, innerContext); return { chain: chain, output: '' }; } }, { /** * Block logic tokens. * * Format: {% use "template.twig" %} */ type: Twig.logic.type.use, parse: function (token, context, chain) { // Resolve filename var file = Twig.expression.parse.call(this, token.stack, context); // Import blocks this.importBlocks(file); return { chain: chain, output: '' }; } }, { /** * Block logic tokens. * * Format: {% includes "template.twig" [with {some: 'values'} only] %} */ type: Twig.logic.type.include, parse: function (token, context, chain) { // Resolve filename var innerContext = {}, withContext, i, template; if (!token.only) { innerContext = Twig.ChildContext(context); } if (token.withStack !== undefined) { withContext = Twig.expression.parse.call(this, token.withStack, context); for (i in withContext) { if (withContext.hasOwnProperty(i)) innerContext[i] = withContext[i]; } } var file = Twig.expression.parse.call(this, token.stack, context); if (file instanceof Twig.Template) { template = file; } else { // Import file try { template = this.importFile(file); } catch (err) { if (token.ignoreMissing) { return { chain: chain, output: '' } } throw err; } } return { chain: chain, output: template.render(innerContext) }; } }, { type: Twig.logic.type.spaceless, // Parse the html and return it without any spaces between tags parse: function (token, context, chain) { var // Parse the output without any filter unfiltered = Twig.parse.call(this, token.output, context), // A regular expression to find closing and opening tags with spaces between them rBetweenTagSpaces = />\s+</g, // Replace all space between closing and opening html tags output = unfiltered.replace(rBetweenTagSpaces, '><').trim(); // Rewrap output as a Twig.Markup output = Twig.Markup(output); return { chain: chain, output: output }; } }, // Add the {% endspaceless %} token { type: Twig.logic.type.endspaceless }, { /** * The embed tag combines the behaviour of include and extends. * It allows you to include another template's contents, just like include does. * * Format: {% embed "template.twig" [with {some: 'values'} only] %} */ type: Twig.logic.type.embed, parse: function (token, context, chain) { // Resolve filename var innerContext = {}, withContext, i, template; if (!token.only) { for (i in context) { if (context.hasOwnProperty(i)) innerContext[i] = context[i]; } } if (token.withStack !== undefined) { withContext = Twig.expression.parse.call(this, token.withStack, context); for (i in withContext) { if (withContext.hasOwnProperty(i)) innerContext[i] = withContext[i]; } } var file = Twig.expression.parse.call(this, token.stack, innerContext); if (file instanceof Twig.Template) { template = file; } else { // Import file try { template = this.importFile(file); } catch (err) { if (token.ignoreMissing) { return { chain: chain, output: '' } } throw err; } } // reset previous blocks this.blocks = {}; // parse tokens. output will be not used var output = Twig.parse.call(this, token.output, innerContext); // render tempalte with blocks defined in embed block return { chain: chain, output: template.render(innerContext, {'blocks': this.blocks}) }; } }, /* Add the {% endembed %} token * */ { type: Twig.logic.type.endembed } ]; /** * Registry for logic handlers. */ Twig.logic.handler = {}; /** * Define a new token type, available at Twig.logic.type.{type} */ Twig.logic.extendType = function (type, value) { value = value || ("Twig.logic.type" + type); Twig.logic.type[type] = value; }; /** * Extend the logic parsing functionality with a new token definition. * * // Define a new tag * Twig.logic.extend({ * type: Twig.logic.type.{type}, * // The pattern to match for this token * regex: ..., * // What token types can follow this token, leave blank if any. * next: [ ... ] * // Create and return compiled version of the token * compile: function(token) { ... } * // Parse the compiled token with the context provided by the render call * // and whether this token chain is complete. * parse: function(token, context, chain) { ... } * }); * * @param {Object} definition The new logic expression. */ Twig.logic.extend = function (definition) { if (!definition.type) { throw new Twig.Error("Unable to extend logic definition. No type provided for " + definition); } else { Twig.logic.extendType(definition.type); } Twig.logic.handler[definition.type] = definition; }; // Extend with built-in expressions while (Twig.logic.definitions.length > 0) { Twig.logic.extend(Twig.logic.definitions.shift()); } /** * Parse a logic token within a given context. * * What are logic chains? * Logic chains represent a series of tokens that are connected, * for example: * {% if ... %} {% else %} {% endif %} * * The chain parameter is used to signify if a chain is open of closed. * open: * More tokens in this chain should be parsed. * closed: * This token chain has completed parsing and any additional * tokens (else, elseif, etc...) should be ignored. * * @param {Object} token The compiled token. * @param {Object} context The render context. * @param {boolean} chain Is this an open logic chain. If false, that means a * chain is closed and no further cases should be parsed. */ Twig.logic.parse = function (token, context, chain) { var output = '', token_template; context = context || {}; Twig.log.debug("Twig.logic.parse: ", "Parsing logic token ", token); token_template = Twig.logic.handler[token.type]; if (token_template.parse) { output = token_template.parse.call(this, token, context, chain); } return output; }; return Twig; })(Twig); // ## twig.expression.js // // This file handles tokenizing, compiling and parsing expressions. (function (Twig) { "use strict"; /** * Namespace for expression handling. */ Twig.expression = {}; // ## twig.expression.operator.js // // This file handles operator lookups and parsing. (function (Twig) { "use strict"; /** * Operator associativity constants. */ Twig.expression.operator = { leftToRight: 'leftToRight', rightToLeft: 'rightToLeft' }; var containment = function(a, b) { if (b === undefined || b === null) { return null; } else if (b.indexOf !== undefined) { // String return a === b || a !== '' && b.indexOf(a) > -1; } else { var el; for (el in b) { if (b.hasOwnProperty(el) && b[el] === a) { return true; } } return false; } }; /** * Handle operations on the RPN stack. * * Returns the updated stack. */ Twig.expression.operator.parse = function (operator, stack) { Twig.log.trace("Twig.expression.operator.parse: ", "Handling ", operator); var a, b, c; if (operator === '?') { c = stack.pop(); } b = stack.pop(); if (operator !== 'not') { a = stack.pop(); } if (operator !== 'in' && operator !== 'not in') { if (a && Array.isArray(a)) { a = a.length; } if (b && Array.isArray(b)) { b = b.length; } } switch (operator) { case ':': // Ignore break; case '?:': if (Twig.lib.boolval(a)) { stack.push(a); } else { stack.push(b); } break; case '?': if (a === undefined) { //An extended ternary. a = b; b = c; c = undefined; } if (Twig.lib.boolval(a)) { stack.push(b); } else { stack.push(c); } break; case '+': b = parseFloat(b); a = parseFloat(a); stack.push(a + b); break; case '-': b = parseFloat(b); a = parseFloat(a); stack.push(a - b); break; case '*': b = parseFloat(b); a = parseFloat(a); stack.push(a * b); break; case '/': b = parseFloat(b); a = parseFloat(a); stack.push(a / b); break; case '//': b = parseFloat(b); a = parseFloat(a); stack.push(Math.floor(a / b)); break; case '%': b = parseFloat(b); a = parseFloat(a); stack.push(a % b); break; case '~': stack.push( (a != null ? a.toString() : "") + (b != null ? b.toString() : "") ); break; case 'not': case '!': stack.push(!Twig.lib.boolval(b)); break; case '<': stack.push(a < b); break; case '<=': stack.push(a <= b); break; case '>': stack.push(a > b); break; case '>=': stack.push(a >= b); break; case '===': stack.push(a === b); break; case '==': stack.push(a == b); break; case '!==': stack.push(a !== b); break; case '!=': stack.push(a != b); break; case 'or': stack.push(a || b); break; case 'b-or': stack.push(a | b); break; case 'b-xor': stack.push(a ^ b); break; case 'and': stack.push(a && b); break; case 'b-and': stack.push(a & b); break; case '**': stack.push(Math.pow(a, b)); break; case 'not in': stack.push( !containment(a, b) ); break; case 'in': stack.push( containment(a, b) ); break; case '..': stack.push( Twig.functions.range(a, b) ); break; default: debugger; throw new Twig.Error("Failed to parse operator: " + operator + " is an unknown operator."); } }; return Twig; })(Twig); /** * Reserved word that can't be used as variable names. */ Twig.expression.reservedWords = [ "true", "false", "null", "TRUE", "FALSE", "NULL", "_context", "and", "b-and", "or", "b-or", "b-xor", "in", "not in", "if" ]; /** * The type of tokens used in expressions. */ Twig.expression.type = { comma: 'Twig.expression.type.comma', operator: { unary: 'Twig.expression.type.operator.unary', binary: 'Twig.expression.type.operator.binary' }, string: 'Twig.expression.type.string', bool: 'Twig.expression.type.bool', slice: 'Twig.expression.type.slice', array: { start: 'Twig.expression.type.array.start', end: 'Twig.expression.type.array.end' }, object: { start: 'Twig.expression.type.object.start', end: 'Twig.expression.type.object.end' }, parameter: { start: 'Twig.expression.type.parameter.start', end: 'Twig.expression.type.parameter.end' }, subexpression: { start: 'Twig.expression.type.subexpression.start', end: 'Twig.expression.type.subexpression.end' }, key: { period: 'Twig.expression.type.key.period', brackets: 'Twig.expression.type.key.brackets' }, filter: 'Twig.expression.type.filter', _function: 'Twig.expression.type._function', variable: 'Twig.expression.type.variable', number: 'Twig.expression.type.number', _null: 'Twig.expression.type.null', context: 'Twig.expression.type.context', test: 'Twig.expression.type.test' }; Twig.expression.set = { // What can follow an expression (in general) operations: [ Twig.expression.type.filter, Twig.expression.type.operator.unary, Twig.expression.type.operator.binary, Twig.expression.type.array.end, Twig.expression.type.object.end, Twig.expression.type.parameter.end, Twig.expression.type.subexpression.end, Twig.expression.type.comma, Twig.expression.type.test ], expressions: [ Twig.expression.type._function, Twig.expression.type.bool, Twig.expression.type.string, Twig.expression.type.variable, Twig.expression.type.number, Twig.expression.type._null, Twig.expression.type.context, Twig.expression.type.parameter.start, Twig.expression.type.array.start, Twig.expression.type.object.start, Twig.expression.type.subexpression.start, Twig.expression.type.operator.unary ] }; // Most expressions allow a '.' or '[' after them, so we provide a convenience set Twig.expression.set.operations_extended = Twig.expression.set.operations.concat([ Twig.expression.type.key.period, Twig.expression.type.key.brackets, Twig.expression.type.slice]); // Some commonly used compile and parse functions. Twig.expression.fn = { parse: { push: function (token, stack) { stack.pus