UNPKG

rbc-twig-compiler

Version:
1,355 lines (1,193 loc) 98.9 kB
/** * RBC twigjs compiler * * @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.6' }; // ## 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.indexOf = function (arr, searchElement) { return arr.indexOf(searchElement); }; 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' }; /** * 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.output_whitespace_pre, open: '{{-', close: '}}' }, { type: Twig.token.type.output_whitespace_post, open: '{{', close: '-}}' }, { type: Twig.token.type.output_whitespace_both, open: '{{-', close: '-}}' }, { type: Twig.token.type.logic_whitespace_pre, open: '{%-', close: '%}' }, { type: Twig.token.type.logic_whitespace_post, open: '{%', close: '-%}' }, { type: Twig.token.type.logic_whitespace_both, 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, close_position: null, def: null }, i, token_template, first_key_position, close_key_position; for (i=0;i<Twig.token.definitions.length;i++) { token_template = Twig.token.definitions[i]; first_key_position = template.indexOf(token_template.open); close_key_position = template.indexOf(token_template.close); Twig.log.trace("Twig.token.findStart: ", "Searching for ", token_template.open, " found at ", first_key_position); //Special handling for mismatched tokens if (first_key_position >= 0) { //This token matches the template if (token_template.open.length !== token_template.close.length) { //This token has mismatched closing and opening tags if (close_key_position < 0) { //This token's closing tag does not match the template continue; } } } // Does this token occur before any other types? if (first_key_position >= 0 && (output.position === null || first_key_position < output.position)) { output.position = first_key_position; output.def = token_template; output.close_position = close_key_position; } else if (first_key_position >= 0 && output.position !== null && first_key_position === output.position) { /*This token exactly matches another token, greedily match to check if this token has a greater specificity*/ if (token_template.open.length > output.def.open.length) { //This token's opening tag is more specific than the previous match output.position = first_key_position; output.def = token_template; output.close_position = close_key_position; } else if (token_template.open.length === output.def.open.length) { if (token_template.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 (close_key_position >= 0 && close_key_position < output.close_position) { //This token's closing tag exists in the template, //and it occurs sooner than the previous match output.position = first_key_position; output.def = token_template; output.close_position = close_key_position; } } else if (close_key_position >= 0 && close_key_position < output.close_position) { //This token's closing tag is not more specific than the previous match, //but it occurs sooner than the previous match output.position = first_key_position; output.def = token_template; output.close_position = close_key_position; } } } } delete output['close_position']; return output; }; Twig.token.findEnd = function (template, token_def, start) { var end = null, found = false, offset = 0, // String position variables str_pos = null, str_found = null, pos = null, end_offset = null, this_str_pos = null, end_str_pos = null, // For loop variables i, l; while (!found) { str_pos = null; str_found = null; pos = template.indexOf(token_def.close, offset); if (pos >= 0) { end = pos; found = true; } else { // throw an exception throw new Twig.Error("Unable to find closing bracket '" + token_def.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 (token_def.type === Twig.token.type.comment) { break; } // Ignore quotes within raw tag // Fixes #283 if (token_def.type === Twig.token.type.raw) { break; } l = Twig.token.strings.length; for (i = 0; i < l; i += 1) { this_str_pos = template.indexOf(Twig.token.strings[i], offset); if (this_str_pos > 0 && this_str_pos < pos && (str_pos === null || this_str_pos < str_pos)) { str_pos = this_str_pos; str_found = 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 (str_pos !== null) { end_offset = str_pos + 1; end = null; found = false; while (true) { end_str_pos = template.indexOf(str_found, end_offset); if (end_str_pos < 0) { throw "Unclosed string in template"; } // Ignore escaped quotes if (template.substr(end_str_pos - 1, 1) !== "\\") { offset = end_str_pos + 1; break; } else { end_offset = end_str_pos + 1; } } } } return end; }; /** * Convert a template into high-level tokens. */ Twig.tokenize = function (template) { var tokens = [], // An offset for reporting errors locations in the template. error_offset = 0, // The start and type of the first token found in the template. found_token = null, // The end position of the matched token. end = null; while (template.length > 0) { // Find the first occurance of any token type in the template found_token = Twig.token.findStart(template); Twig.log.trace("Twig.tokenize: ", "Found token: ", found_token); if (found_token.position !== null) { // Add a raw type token for anything before the start of the token if (found_token.position > 0) { tokens.push({ type: Twig.token.type.raw, value: template.substring(0, found_token.position) }); } template = template.substr(found_token.position + found_token.def.open.length); error_offset += found_token.position + found_token.def.open.length; // Find the end of the token end = Twig.token.findEnd(template, found_token.def, error_offset); Twig.log.trace("Twig.tokenize: ", "Token ends at ", end); tokens.push({ type: found_token.def.type, value: template.substring(0, end).trim() }); if (template.substr( end + found_token.def.close.length, 1 ) === "\n") { switch (found_token.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; } } template = template.substr(end + found_token.def.close.length); // Increment the position in the template error_offset += end + found_token.def.close.length; } else { // No more tokens -> add the rest of the template as a raw-type token tokens.push({ type: Twig.token.type.raw, value: template }); template = ''; } } return tokens; }; Twig.compile = function (tokens) { try { // Output and intermediate stacks var output = [], stack = [], // The tokens between open and close tags intermediate_output = [], token = null, logic_token = null, unclosed_token = null, // Temporary previous token. prev_token = null, // Temporary previous output. prev_output = null, // Temporary previous intermediate output. prev_intermediate_output = null, // The previous token's template prev_template = null, // Token lookahead next_token = null, // The output token tok_output = null, // Logic Token values type = null, open = null, next = null; var compile_output = function(token) { Twig.expression.compile.call(this, token); if (stack.length > 0) { intermediate_output.push(token); } else { output.push(token); } }; var compile_logic = function(token) { // Compile the logic token logic_token = Twig.logic.compile.call(this, token); type = logic_token.type; open = Twig.logic.handler[type].open; next = Twig.logic.handler[type].next; Twig.log.trace("Twig.compile: ", "Compiled logic token to ", logic_token, " next is: ", next, " open is : ", open); // Not a standalone token, check logic stack to see if this is expected if (open !== undefined && !open) { prev_token = stack.pop(); prev_template = Twig.logic.handler[prev_token.type]; if (Twig.indexOf(prev_template.next, type) < 0) { throw new Error(type + " not expected after a " + prev_token.type); } prev_token.output = prev_token.output || []; prev_token.output = prev_token.output.concat(intermediate_output); intermediate_output = []; tok_output = { type: Twig.token.type.logic, token: prev_token }; if (stack.length > 0) { intermediate_output.push(tok_output); } else { output.push(tok_output); } } // This token requires additional tokens to complete the logic structure. if (next !== undefined && next.length > 0) { Twig.log.trace("Twig.compile: ", "Pushing ", logic_token, " 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. prev_token = stack.pop(); prev_token.output = prev_token.output || []; prev_token.output = prev_token.output.concat(intermediate_output); stack.push(prev_token); intermediate_output = []; } // Push the new logic token onto the logic stack stack.push(logic_token); } else if (open !== undefined && open) { tok_output = { type: Twig.token.type.logic, token: logic_token }; // Standalone token (like {% set ... %} if (stack.length > 0) { intermediate_output.push(tok_output); } else { output.push(tok_output); } } }; while (tokens.length > 0) { token = tokens.shift(); prev_output = output[output.length - 1]; prev_intermediate_output = intermediate_output[intermediate_output.length - 1]; next_token = tokens[0]; Twig.log.trace("Compiling token ", token); switch (token.type) { case Twig.token.type.raw: if (stack.length > 0) { intermediate_output.push(token); } else { output.push(token); } break; case Twig.token.type.logic: compile_logic.call(this, token); break; // Do nothing, comments should be ignored case Twig.token.type.comment: break; case Twig.token.type.output: compile_output.call(this, token); break; //Kill whitespace ahead and behind this token case Twig.token.type.logic_whitespace_pre: case Twig.token.type.logic_whitespace_post: case Twig.token.type.logic_whitespace_both: case Twig.token.type.output_whitespace_pre: case Twig.token.type.output_whitespace_post: case Twig.token.type.output_whitespace_both: if (token.type !== Twig.token.type.output_whitespace_post && token.type !== Twig.token.type.logic_whitespace_post) { if (prev_output) { //If the previous output is raw, pop it off if (prev_output.type === Twig.token.type.raw) { output.pop(); //If the previous output is not just whitespace, trim it if (prev_output.value.match(/^\s*$/) === null) { prev_output.value = prev_output.value.trim(); //Repush the previous output output.push(prev_output); } } } if (prev_intermediate_output) { //If the previous intermediate output is raw, pop it off if (prev_intermediate_output.type === Twig.token.type.raw) { intermediate_output.pop(); //If the previous output is not just whitespace, trim it if (prev_intermediate_output.value.match(/^\s*$/) === null) { prev_intermediate_output.value = prev_intermediate_output.value.trim(); //Repush the previous intermediate output intermediate_output.push(prev_intermediate_output); } } } } //Compile this token switch (token.type) { case Twig.token.type.output_whitespace_pre: case Twig.token.type.output_whitespace_post: case Twig.token.type.output_whitespace_both: compile_output.call(this, token); break; case Twig.token.type.logic_whitespace_pre: case Twig.token.type.logic_whitespace_post: case Twig.token.type.logic_whitespace_both: compile_logic.call(this, token); break; } if (token.type !== Twig.token.type.output_whitespace_pre && token.type !== Twig.token.type.logic_whitespace_pre) { if (next_token) { //If the next token is raw, shift it out if (next_token.type === Twig.token.type.raw) { tokens.shift(); //If the next token is not just whitespace, trim it if (next_token.value.match(/^\s*$/) === null) { next_token.value = next_token.value.trim(); //Unshift the next token tokens.unshift(next_token); } } } } break; } Twig.log.trace("Twig.compile: ", " Output: ", output, " Logic Stack: ", stack, " Pending Output: ", intermediate_output ); } // Verify that there are no logic tokens left in the stack. if (stack.length > 0) { unclosed_token = stack.pop(); throw new Error("Unable to find an end tag for " + unclosed_token.type + ", expecting one of " + unclosed_token.next); } return output; } catch (ex) { if (this.options.rethrow) { throw ex } else { Twig.log.error("Error compiling twig template " + this.id + ": "); if (ex.stack) { Twig.log.error(ex.stack); } else { Twig.log.error(ex.toString()); } } } }; /** * Tokenize and compile a string template. * * @param {string} data The template. * * @return {Array} The compiled tokens. */ Twig.prepare = function(data) { var tokens, raw_tokens; // Tokenize Twig.log.debug("Twig.prepare: ", "Tokenizing ", data); raw_tokens = Twig.tokenize.call(this, data); // Compile Twig.log.debug("Twig.prepare: ", "Compiling ", raw_tokens); tokens = Twig.compile.call(this, raw_tokens); Twig.log.debug("Twig.prepare: ", "Compiled ", tokens); return tokens; }; // 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; }; // Determine object type function is(type, obj) { var clas = Object.prototype.toString.call(obj).slice(8, -1); return obj !== undefined && obj !== null && clas === type; } /** * 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, 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.name = name; this.options = options; this.reset(blocks); if (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; }; /** * 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.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' }; /** * Get the precidence and associativity of an operator. These follow the order that C/C++ use. * See http://en.wikipedia.org/wiki/Operators_in_C_and_C++ for the table of values. */ Twig.expression.operator.lookup = function (operator, token) { switch (operator) { case "..": token.precidence = 20; token.associativity = Twig.expression.operator.leftToRight; break; case ',': token.precidence = 18; token.associativity = Twig.expression.operator.leftToRight; break; // Ternary case '?:': case '?': case ':': token.precidence = 16; token.associativity = Twig.expression.operator.rightToLeft; break; case 'or': token.precidence = 14; token.associativity = Twig.expression.operator.leftToRight; break; case 'and': token.precidence = 13; token.associativity = Twig.expression.operator.leftToRight; break; case 'b-or': token.precidence = 12; token.associativity = Twig.expression.operator.leftToRight; break; case 'b-xor': token.precidence = 11; token.associativity = Twig.expression.operator.leftToRight; break; case 'b-and': token.precidence = 10; token.associativity = Twig.expression.operator.leftToRight; break; case '==': case '!=': token.precidence = 9; token.associativity = Twig.expression.operator.leftToRight; break; case '<': case '<=': case '>': case '>=': case 'not in': case 'in': token.precidence = 8; token.associativity = Twig.expression.operator.leftToRight; break; case '~': // String concatination case '+': case '-': token.precidence = 6; token.associativity = Twig.expression.operator.leftToRight; break; case '//': case '**': case '*': case '/': case '%': token.precidence = 5; token.associativity = Twig.expression.operator.leftToRight; break; case 'not': token.precidence = 3; token.associativity = Twig.expression.operator.rightToLeft; break; default: throw new Twig.Error("Failed to lookup operator: " + operator + " is an unknown operator."); } token.operator = operator; return token; }; return Twig; })(Twig); /** * Reserved word that can't be used as variable names. */ Twig.expression.reservedWords = [ "true", "false", "null", "TRUE", "FALSE", "NULL", "_context", "and", "or", "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 = { compile: { push: function(token, stack, output) { output.push(token); }, push_both: function(token, stack, output) { output.push(token); stack.push(token); } } }; // The regular expressions and compile/parse logic used to match tokens in expressions. // // Properties: // // type: The type of expression this matches // // regex: One or more regular expressions that matche the format of the token. // // next: Valid tokens that can occur next in the expression. // // Functions: // // compile: A function that compiles the raw regular expression match into a token. // // parse: A function that parses the compiled token into output. // Twig.expression.definitions = [ { type: Twig.expression.type.test, regex: /^is\s+(not)?\s*([a-zA-Z_][a-zA-Z0-9_]*(\s?as)?)/, next: Twig.expression.set.operations.concat([Twig.expression.type.parameter.start]), compile: function(token, stack, output) { token.filter = token.match[2]; token.modifier = token.match[1]; delete token.match; delete token.value; output.push(token); } }, { type: Twig.expression.type.comma, // Match a comma regex: /^,/, next: Twig.expression.set.expressions.concat([Twig.expression.type.array.end, Twig.expression.type.object.end]), compile: function(token, stack, output) { var i = stack.length - 1, stack_token; delete token.match; delete token.value; // pop tokens off the stack until the start of the object for(;i >= 0; i--) { stack_token = stack.pop(); if (stack_token.type === Twig.expression.type.object.start || stack_token.type === Twig.expression.type.parameter.start || stack_token.type === Twig.expression.type.array.start) { stack.push(stack_token); break; } output.push(stack_token); } output.push(token); } }, { /** * Match a number (integer or decimal) */ type: Twig.expression.type.number, // match a number regex: /^\-?\d+(\.\d+)?/, next: Twig.expression.set.operations, compile: function(token, stack, output) { token.value = Number(token.value); output.push(token); } }, { type: Twig.expression.type.operator.binary, // Match any of ?:, +, *, /, -, %, ~, <, <=, >, >=, !=, ==, **, ?, :, and, b-and, or, b-or, b-xor, in, not in // and, or, in, not in can be followed by a space or parenthesis regex: /(^\?\:|^(b\-and)|^(b\-or)|^(b\-xor)|^[\+\-~%\?]|^[\:](?!\d\])|^[!=]==?|^[!<>]=?|^\*\*?|^\/\/?|^(and)[\(|\s+]|^(or)[\(|\s+]|^(in)[\(|\s+]|^(not in)[\(|\s+]|^\.\.)/, next: Twig.expression.set.expressions, transform: function(match, tokens) { switch(match[0]) { case 'and(': case 'or(': case 'in(': case 'not in(': //Strip off the ( if it exists tokens[tokens.length - 1].value = match[2]; return match[0]; break; default: return ''; } }, compile: function(token, stack, output) { delete token.match; token.value = token.value.trim(); var value = token.value, operator = Twig.expression.operator.lookup(value, token); Twig.log.trace("Twig.expression.compile: ", "Operator: ", operator, " from ", value); while (stack.length > 0 && (stack[stack.length-1].type == Twig.expression.type.operator.unary || stack[stack.length-1].type == Twig.expression.type.operator.binary) && ( (operator.associativity === Twig.expression.operator.leftToRight && operator.precidence >= stack[stack.length-1].precidence) || (operator.associativity === Twig.expression.operator.rightToLeft && operator.precidence > stack[stack.length-1].precidence) ) ) { var temp = stack.pop(); output.push(temp); } if (value === ":") { // Check if this is a ternary or object key being set if (stack[stack.length - 1] && stack[stack.length-1].value === "?") { // Continue as normal for a ternary } else { // This is not a ternary so we push the token to the output where it can be handled // when the assocated object is closed. var key_token = output.pop(); if (key_token.type === Twig.expression.type.string || key_token.type === Twig.expression.type.variable) { token.key = key_token.value; } else if (key_token.type === Twig.expression.type.number) { // Convert integer keys into string keys token.key = key_token.value.toString(); } else if (key_token.expression && (key_token.type === Twig.expression.type.parameter.end || key_token.type == Twig.expression.type.subexpression.end)) { token.params = key_token.params; } else { throw new Twig.Error("Unexpected value before ':' of " + key_token.type + " = " + key_token.value); } output.push(token); return; } } else { stack.push(operator); } } }, { type: Twig.expression.type.operator.unary, // Match any of not regex: /(^not\s+)/, next: Twig.expression.set.expressions, compile: function(token, stack, output) { delete token.match; token.value = token.value.trim(); var value = token.value, operator = Twig.expression.operator.lookup(value, token); Twig.log.trace("Twig.expression.compile: ", "Operator: ", operator, " from ", value); while (stack.length > 0 && (stack[stack.length-1].type == Twig.expression.type.operator.unary || stack[stack.length-1].type == Twig.expression.type.operator.binary) && ( (operator.associativity === Twig.expression.operator.leftToRight && operator.precidence >= stack[stack.length-1].precidence) || (operator.associativity === Twig.expression.operator.rightToLeft && operator.precidence > stack[stack.length-1].precidence) ) ) { var temp = stack.pop(); output.push(temp); } stack.push(operator); } }, { /** * Match a string. This is anything between a pair of single or double quotes. */ type: Twig.expression.type.string, // See: http://blog.stevenlevithan.com/archives/match-quoted-string regex: /^(["'])(?:(?=(\\?))\2[\s\S])*?\1/, next: Twig.expression.set.operations_extended, compile: function(token, stack, output) { var value = token.value; delete token.match // Remove the quotes from the string if (value.substring(0, 1) === '"') { value = value.replace('\\"', '"'); } else { value = value.replace("\\'", "'"); } token.value = value.substring(1, value.length-1).replace( /\\n/g, "\n" ).replace( /\\r/g, "\r" ); Twig.log.trace("Twig.expression.compile: ", "String value: ", token.value); output.push(token); } }, { /** * Match a subexpression set start. */ type: Twig.expression.type.subexpression.start, regex: /^\(/, next: Twig.expression.set.expressions.concat([Twig.expression.type.subexpression.end]), compile: function(token, stack, output) { token.value = '('; output.push(token); stack.push(token); } }, { /** * Match a subexpression set end. */ type: Twig.expression.type.subexpression.end, regex: /^\)/, next: Twig.expression.set.operations_extended, validate: function(match, tokens) { // Iterate back through previous tokens to ensure we follow a subexpression start var i = tokens.length - 1, found_subexpression_start = false, next_subexpression_start_invalid = false, unclosed_parameter_count = 0; while(!found_subexpression_start && i >= 0) { var token = tokens[i]; found_subexpression_start = token.type === Twig.expression.type.subexpression.start; // If we have previously found a subexpression end, then this subexpression start is the start of // that subexpression, not the subexpression we are searching for if (found_subexpression_start && next_subexpression_start_invalid) { next_subexpression_start_invalid = false; found_subexpression_start = false; } // Count parameter tokens to ensure we dont return truthy for a