twig.expression.js | |
---|---|
| |
twig.expression.jsThis file handles tokenizing, compiling and parsing expressions. | var Twig = (function (Twig) {
"use strict";
/**
* Namespace for expression handling.
*/
Twig.expression = { };
/**
* Reserved word that can't be used as variable names.
*/
Twig.expression.reservedWords = [
"true", "false", "null"
];
/**
* The type of tokens used in expressions.
*/
Twig.expression.type = {
comma: 'Twig.expression.type.comma',
expression: 'Twig.expression.type.expression',
operator: {
unary: 'Twig.expression.type.operator.unary',
binary: 'Twig.expression.type.operator.binary'
},
string: 'Twig.expression.type.string',
bool: 'Twig.expression.type.bool',
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'
},
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',
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.comma,
Twig.expression.type.test
],
expressions: [
Twig.expression.type._function,
Twig.expression.type.expression,
Twig.expression.type.bool,
Twig.expression.type.string,
Twig.expression.type.variable,
Twig.expression.type.number,
Twig.expression.type._null,
Twig.expression.type.array.start,
Twig.expression.type.object.start
]
}; |
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]); |
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);
}
},
parse: {
push: function(token, stack, context) {
stack.push(token);
},
push_value: function(token, stack, context) {
stack.push(token.value);
}
}
}; |
The regular expressions and compile/parse logic used to match tokens in expressions. Properties:
Functions: | Twig.expression.definitions = [
{
type: Twig.expression.type.test,
regex: /^is\s+(not)?\s*([a-zA-Z_][a-zA-Z0-9_]*)/,
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);
},
parse: function(token, stack, context) {
var value = stack.pop(),
params = token.params && Twig.expression.parse.apply(this, [token.params, context]),
result = Twig.test(token.filter, value, params);
if (token.modifier == 'not') {
stack.push(!result);
} else {
stack.push(result);
}
}
},
{
type: Twig.expression.type.comma, |
Match a comma | regex: /^,/,
next: Twig.expression.set.expressions,
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);
}
},
{
type: Twig.expression.type.expression, |
Match (, anything but ), ) | regex: /^\(([^\)]+)\)/,
next: Twig.expression.set.operations_extended,
compile: function(token, stack, output) {
token.value = token.match[1];
var sub_stack = Twig.expression.compile(token).stack;
while (sub_stack.length > 0) {
output.push(sub_stack.shift());
}
}
},
{
type: Twig.expression.type.operator.binary, |
Match any of +, , /, -, %, ~, <, <=, >, >=, !=, ==, *, ?, :, and, or, not | regex: /(^[\+\-~%\?\:]|^[!=]==?|^[!<>]=?|^\*\*?|^\/\/?|^and\s+|^or\s+|^in\s+|^not in\s+|^\.\.)/,
next: Twig.expression.set.expressions.concat([Twig.expression.type.operator.unary]),
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 ||
key_token.type === Twig.expression.type.number) {
token.key = key_token.value;
} else {
throw new Twig.Error("Unexpected value before ':' of " + key_token.type + " = " + key_token.value);
}
output.push(token);
return;
}
} else {
stack.push(operator);
}
},
parse: function(token, stack, context) {
if (token.key) { |
handle ternary ':' operator | stack.push(token);
} else {
Twig.expression.operator.parse(token.value, stack);
}
}
},
{
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);
},
parse: function(token, stack, context) {
Twig.expression.operator.parse(token.value, stack);
}
},
{
/**
* 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.)*?\1/,
next: Twig.expression.set.operations,
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);
Twig.log.trace("Twig.expression.compile: ", "String value: ", token.value);
output.push(token);
},
parse: Twig.expression.fn.parse.push_value
},
{
/**
* Match a parameter set start.
*/
type: Twig.expression.type.parameter.start,
regex: /^\(/,
next: Twig.expression.set.expressions.concat([Twig.expression.type.parameter.end]),
compile: Twig.expression.fn.compile.push_both,
parse: Twig.expression.fn.parse.push
},
{
/**
* Match a parameter set end.
*/
type: Twig.expression.type.parameter.end,
regex: /^\)/,
next: Twig.expression.set.operations_extended,
compile: function(token, stack, output) {
var stack_token;
stack_token = stack.pop();
while(stack.length > 0 && stack_token.type != Twig.expression.type.parameter.start) {
output.push(stack_token);
stack_token = stack.pop();
} |
Move contents of parens into preceding filter | var param_stack = [];
while(token.type !== Twig.expression.type.parameter.start) { |
Add token to arguments stack | param_stack.unshift(token);
token = output.pop();
}
param_stack.unshift(token); |
Get the token preceding the parameters | token = output.pop();
if (token.type !== Twig.expression.type._function &&
token.type !== Twig.expression.type.filter &&
token.type !== Twig.expression.type.test &&
token.type !== Twig.expression.type.key.brackets &&
token.type !== Twig.expression.type.key.period) {
throw new Twig.Error("Expected filter or function before parameters, got " + token.type);
}
token.params = param_stack;
output.push(token);
},
parse: function(token, stack, context) {
var new_array = [],
array_ended = false,
value = null;
while (stack.length > 0) {
value = stack.pop(); |
Push values into the array until the start of the array | if (value && value.type && value.type == Twig.expression.type.parameter.start) {
array_ended = true;
break;
}
new_array.unshift(value);
}
if (!array_ended) {
throw new Twig.Error("Expected end of parameter set.");
}
stack.push(new_array);
}
},
{
/**
* Match an array start.
*/
type: Twig.expression.type.array.start,
regex: /^\[/,
next: Twig.expression.set.expressions.concat([Twig.expression.type.array.end]),
compile: Twig.expression.fn.compile.push_both,
parse: Twig.expression.fn.parse.push
},
{
/**
* Match an array end.
*/
type: Twig.expression.type.array.end,
regex: /^\]/,
next: Twig.expression.set.operations_extended,
compile: function(token, stack, output) {
var i = stack.length - 1,
stack_token; |
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.array.start) {
break;
}
output.push(stack_token);
}
output.push(token);
},
parse: function(token, stack, context) {
var new_array = [],
array_ended = false,
value = null;
while (stack.length > 0) {
value = stack.pop(); |
Push values into the array until the start of the array | if (value.type && value.type == Twig.expression.type.array.start) {
array_ended = true;
break;
}
new_array.unshift(value);
}
if (!array_ended) {
throw new Twig.Error("Expected end of array.");
}
stack.push(new_array);
}
}, |
Token that represents the start of a hash map '}' Hash maps take the form: { "key": 'value', "another_key": item } Keys must be quoted (either single or double) and values can be any expression. | {
type: Twig.expression.type.object.start,
regex: /^\{/,
next: Twig.expression.set.expressions.concat([Twig.expression.type.object.end]),
compile: Twig.expression.fn.compile.push_both,
parse: Twig.expression.fn.parse.push
}, |
Token that represents the end of a Hash Map '}' This is where the logic for building the internal representation of a hash map is defined. | {
type: Twig.expression.type.object.end,
regex: /^\}/,
next: Twig.expression.set.operations_extended,
compile: function(token, stack, output) {
var i = stack.length-1,
stack_token; |
pop tokens off the stack until the start of the object | for(;i >= 0; i--) {
stack_token = stack.pop();
if (stack_token && stack_token.type === Twig.expression.type.object.start) {
break;
}
output.push(stack_token);
}
output.push(token);
},
parse: function(end_token, stack, context) {
var new_object = {},
object_ended = false,
token = null,
token_key = null,
has_value = false,
value = null;
while (stack.length > 0) {
token = stack.pop(); |
Push values into the array until the start of the object | if (token && token.type && token.type === Twig.expression.type.object.start) {
object_ended = true;
break;
}
if (token && token.type && (token.type === Twig.expression.type.operator.binary || token.type === Twig.expression.type.operator.unary) && token.key) {
if (!has_value) {
throw new Twig.Error("Missing value for key '" + token.key + "' in object definition.");
}
new_object[token.key] = value; |
Preserve the order that elements are added to the map This is necessary since JavaScript objects don't guarantee the order of keys | if (new_object._keys === undefined) new_object._keys = [];
new_object._keys.unshift(token.key); |
reset value check | value = null;
has_value = false;
} else {
has_value = true;
value = token;
}
}
if (!object_ended) {
throw new Twig.Error("Unexpected end of object.");
}
stack.push(new_object);
}
}, |
Token representing a filter Filters can follow any expression and take the form: expression|filter(optional, args) Filter parsing is done in the Twig.filters namespace. | {
type: Twig.expression.type.filter, |
match a | then a letter or _, then any number of letters, numbers, _ or - | regex: /^\|\s?([a-zA-Z_][a-zA-Z0-9_\-]*)/,
next: Twig.expression.set.operations_extended.concat([
Twig.expression.type.parameter.start]),
compile: function(token, stack, output) {
token.value = token.match[1];
output.push(token);
},
parse: function(token, stack, context) {
var input = stack.pop(),
params = token.params && Twig.expression.parse.apply(this, [token.params, context]);
stack.push(Twig.filter.apply(this, [token.value, input, params]));
}
},
{
type: Twig.expression.type._function, |
match any letter or _, then any number of letters, numbers, _ or - followed by ( | regex: /^([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/,
next: Twig.expression.type.parameter.start,
transform: function(match, tokens) {
return '(';
},
compile: function(token, stack, output) {
var fn = token.match[1];
token.fn = fn; |
cleanup token | delete token.match;
delete token.value;
output.push(token);
},
parse: function(token, stack, context) {
var params = token.params && Twig.expression.parse.apply(this, [token.params, context]),
fn = token.fn,
value;
if (Twig.functions[fn]) { |
Get the function from the built-in functions | value = Twig.functions[fn].apply(this, params);
} else if (typeof context[fn] == 'function') { |
Get the function from the user/context defined functions | value = context[fn].apply(context, params);
} else {
throw new Twig.Error(fn + ' function does not exist and is not defined in the context');
}
stack.push(value);
}
}, |
Token representing a variable. Variables can contain letters, numbers, underscores and dashes, but must start with a letter or underscore. Variables are retrieved from the render context and take the value of 'undefined' if the given variable doesn't exist in the context. | {
type: Twig.expression.type.variable, |
match any letter or _, then any number of letters, numbers, _ or - | regex: /^[a-zA-Z_][a-zA-Z0-9_]*/,
next: Twig.expression.set.operations_extended.concat([
Twig.expression.type.parameter.start]),
compile: Twig.expression.fn.compile.push,
validate: function(match, tokens) {
return Twig.expression.reservedWords.indexOf(match[0]) == -1;
},
parse: function(token, stack, context) { |
Get the variable from the context | var value = Twig.expression.resolve(context[token.value], context);
stack.push(value);
}
},
{
type: Twig.expression.type.key.period,
regex: /^\.([a-zA-Z0-9_]+)/,
next: Twig.expression.set.operations_extended.concat([
Twig.expression.type.parameter.start]),
compile: function(token, stack, output) {
token.key = token.match[1];
delete token.match;
delete token.value;
output.push(token);
},
parse: function(token, stack, context) {
var params = token.params && Twig.expression.parse.apply(this, [token.params, context]),
key = token.key,
object = stack.pop(),
value;
if (object === null || object === undefined) {
if (this.options.strict_variables) {
throw new Twig.Error("Can't access a key " + key + " on an null or undefined object.");
} else {
return null;
}
}
var capitalize = function(value) {return value.substr(0, 1).toUpperCase() + value.substr(1);}; |
Get the variable from the context | if (typeof object === 'object' && key in object) {
value = object[key];
} else if (object["get"+capitalize(key)] !== undefined) {
value = object["get"+capitalize(key)];
} else if (object["is"+capitalize(key)] !== undefined) {
value = object["is"+capitalize(key)];
} else {
value = null;
}
stack.push(Twig.expression.resolve(value, object, params));
}
},
{
type: Twig.expression.type.key.brackets,
regex: /^\[([^\]]*)\]/,
next: Twig.expression.set.operations_extended.concat([
Twig.expression.type.parameter.start]),
compile: function(token, stack, output) {
var match = token.match[1];
delete token.value;
delete token.match; |
The expression stack for the key | token.stack = Twig.expression.compile({
value: match
}).stack;
output.push(token);
},
parse: function(token, stack, context) { |
Evaluate key | var params = token.params && Twig.expression.parse.apply(this, [token.params, context]),
key = Twig.expression.parse.apply(this, [token.stack, context]),
object = stack.pop(),
value;
if (object === null || object === undefined) {
if (this.options.strict_variables) {
throw new Twig.Error("Can't access a key " + key + " on an null or undefined object.");
} else {
return null;
}
} |
Get the variable from the context | if (typeof object === 'object' && key in object) {
value = object[key];
} else {
value = null;
}
stack.push(Twig.expression.resolve(value, object, params));
}
},
{
/**
* Match a null value.
*/
type: Twig.expression.type._null, |
match a number | regex: /^null/,
next: Twig.expression.set.operations,
compile: function(token, stack, output) {
delete token.match;
token.value = null;
output.push(token);
},
parse: Twig.expression.fn.parse.push_value
},
{
/**
* 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);
},
parse: Twig.expression.fn.parse.push_value
},
{
/**
* Match a boolean
*/
type: Twig.expression.type.bool,
regex: /^(true|false)/,
next: Twig.expression.set.operations,
compile: function(token, stack, output) {
token.value = (token.match[0] == "true");
delete token.match;
output.push(token);
},
parse: Twig.expression.fn.parse.push_value
}
];
/**
* Resolve a context value.
*
* If the value is a function, it is executed with a context parameter.
*
* @param {string} key The context object key.
* @param {Object} context The render context.
*/
Twig.expression.resolve = function(value, context, params) {
if (typeof value == 'function') {
return value.apply(context, params || []);
} else {
return value;
}
};
/**
* Registry for logic handlers.
*/
Twig.expression.handler = {};
/**
* Define a new expression type, available at Twig.logic.type.{type}
*
* @param {string} type The name of the new type.
*/
Twig.expression.extendType = function (type) {
Twig.expression.type[type] = "Twig.expression.type." + type;
};
/**
* Extend the expression parsing functionality with a new definition.
*
* Token definitions follow this format:
* {
* type: One of Twig.expression.type.[type], either pre-defined or added using
* Twig.expression.extendType
*
* next: Array of types from Twig.expression.type that can follow this token,
*
* regex: A regex or array of regex's that should match the token.
*
* compile: function(token, stack, output) called when this token is being compiled.
* Should return an object with stack and output set.
*
* parse: function(token, stack, context) called when this token is being parsed.
* Should return an object with stack and context set.
* }
*
* @param {Object} definition A token definition.
*/
Twig.expression.extend = function (definition) {
if (!definition.type) {
throw new Twig.Error("Unable to extend logic definition. No type provided for " + definition);
}
Twig.expression.handler[definition.type] = definition;
}; |
Extend with built-in expressions | while (Twig.expression.definitions.length > 0) {
Twig.expression.extend(Twig.expression.definitions.shift());
}
/**
* Break an expression into tokens defined in Twig.expression.definitions.
*
* @param {string} expression The string to tokenize.
*
* @return {Array} An array of tokens.
*/
Twig.expression.tokenize = function (expression) {
var tokens = [], |
Keep an offset of the location in the expression for error messages. | exp_offset = 0, |
The valid next tokens of the previous token | next = null, |
Match information | type, regex, regex_array, |
The possible next token for the match | token_next, |
Has a match been found from the definitions | match_found, invalid_matches = [], match_function;
match_function = function () {
var match = Array.prototype.slice.apply(arguments),
string = match.pop(),
offset = match.pop();
Twig.log.trace("Twig.expression.tokenize",
"Matched a ", type, " regular expression of ", match);
if (next && next.indexOf(type) < 0) {
invalid_matches.push(
type + " cannot follow a " + tokens[tokens.length - 1].type +
" at template:" + exp_offset + " near '" + match[0].substring(0, 20) +
"...'"
); |
Not a match, don't change the expression | return match[0];
} |
Validate the token if a validation function is provided | if (Twig.expression.handler[type].validate &&
!Twig.expression.handler[type].validate(match, tokens)) {
return match[0];
}
invalid_matches = [];
tokens.push({
type: type,
value: match[0],
match: match
});
match_found = true;
next = token_next;
exp_offset += match[0].length; |
Does the token need to return output back to the expression string e.g. a function match of cycle( might return the '(' back to the expression This allows look-ahead to differentiate between token types (e.g. functions and variable names) | if (Twig.expression.handler[type].transform) {
return Twig.expression.handler[type].transform(match, tokens);
}
return '';
};
Twig.log.debug("Twig.expression.tokenize", "Tokenizing expression ", expression);
while (expression.length > 0) {
expression = expression.trim();
for (type in Twig.expression.handler) {
if (Twig.expression.handler.hasOwnProperty(type)) {
token_next = Twig.expression.handler[type].next;
regex = Twig.expression.handler[type].regex; |
Twig.log.trace("Checking type ", type, " on ", expression); | if (regex instanceof Array) {
regex_array = regex;
} else {
regex_array = [regex];
}
match_found = false;
while (regex_array.length > 0) {
regex = regex_array.pop();
expression = expression.replace(regex, match_function);
} |
An expression token has been matched. Break the for loop and start trying to match the next template (if expression isn't empty.) | if (match_found) {
break;
}
}
}
if (!match_found) {
if (invalid_matches.length > 0) {
throw new Twig.Error(invalid_matches.join(" OR "));
} else {
throw new Twig.Error("Unable to parse '" + expression + "' at template position" + exp_offset);
}
}
}
Twig.log.trace("Twig.expression.tokenize", "Tokenized to ", tokens);
return tokens;
};
/**
* Compile an expression token.
*
* @param {Object} raw_token The uncompiled token.
*
* @return {Object} The compiled token.
*/
Twig.expression.compile = function (raw_token) {
var expression = raw_token.value, |
Tokenize expression | tokens = Twig.expression.tokenize(expression),
token = null,
output = [],
stack = [],
token_template = null;
Twig.log.trace("Twig.expression.compile: ", "Compiling ", expression); |
Push tokens into RPN stack using the Sunting-yard algorithm See http://en.wikipedia.org/wiki/Shuntingyardalgorithm | while (tokens.length > 0) {
token = tokens.shift();
token_template = Twig.expression.handler[token.type];
Twig.log.trace("Twig.expression.compile: ", "Compiling ", token); |
Compile the template | token_template.compile && token_template.compile(token, stack, output);
Twig.log.trace("Twig.expression.compile: ", "Stack is", stack);
Twig.log.trace("Twig.expression.compile: ", "Output is", output);
}
while(stack.length > 0) {
output.push(stack.pop());
}
Twig.log.trace("Twig.expression.compile: ", "Final output is", output);
raw_token.stack = output;
delete raw_token.value;
return raw_token;
};
/**
* Parse an RPN expression stack within a context.
*
* @param {Array} tokens An array of compiled expression tokens.
* @param {Object} context The render context to parse the tokens with.
*
* @return {Object} The result of parsing all the tokens. The result
* can be anything, String, Array, Object, etc... based on
* the given expression.
*/
Twig.expression.parse = function (tokens, context) {
var that = this; |
If the token isn't an array, make it one. | if (!(tokens instanceof Array)) {
tokens = [tokens];
} |
The output stack | var stack = [],
token_template = null;
tokens.forEach(function (token) {
token_template = Twig.expression.handler[token.type];
token_template.parse && token_template.parse.apply(that, [token, stack, context]);
}); |
Pop the final value off the stack | return stack.pop();
};
return Twig;
})( Twig || { } );
|