rbc-twig-compiler
Version:
Pre-compiled Twig.js templates
1,355 lines (1,193 loc) • 98.9 kB
JavaScript
/**
* 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