twig.logic.js | |
---|---|
| |
twig.logic.jsThis file handles tokenizing, compiling and parsing logic tokens. {% ... %} | var Twig = (function (Twig) {
"use strict";
/**
* Namespace for logic handling.
*/
Twig.logic = {};
/**
* Logic token types.
*/
Twig.logic.type = {
if_: 'Twig.logic.type.if',
endif: 'Twig.logic.type.endif',
for_: 'Twig.logic.type.for',
endfor: 'Twig.logic.type.endfor',
else_: 'Twig.logic.type.else',
elseif: 'Twig.logic.type.elseif',
set: 'Twig.logic.type.set',
filter: 'Twig.logic.type.filter',
endfilter: 'Twig.logic.type.endfilter',
block: 'Twig.logic.type.block',
endblock: 'Twig.logic.type.endblock',
extends_: 'Twig.logic.type.extends',
use: 'Twig.logic.type.use',
include: 'Twig.logic.type.include'
}; |
Regular expressions for handling logic tokens. Properties:
Functions: | Twig.logic.definitions = [
{
/**
* If type logic tokens.
*
* Format: {% if expression %}
*/
type: Twig.logic.type.if_,
regex: /^if\s+([^\s].+)$/,
next: [
Twig.logic.type.else_,
Twig.logic.type.elseif,
Twig.logic.type.endif
],
open: true,
compile: function (token) {
var expression = token.match[1]; |
Compile the expression. | token.stack = Twig.expression.compile.apply(this, [{
type: Twig.expression.type.expression,
value: expression
}]).stack;
delete token.match;
return token;
},
parse: function (token, context, chain) {
var output = '', |
Parse the expression | result = Twig.expression.parse.apply(this, [token.stack, context]); |
Start a new logic chain | chain = true;
if (result) {
chain = false; |
parse if output | output = Twig.parse.apply(this, [token.output, context]);
}
return {
chain: chain,
output: output
};
}
},
{
/**
* Else if type logic tokens.
*
* Format: {% elseif expression %}
*/
type: Twig.logic.type.elseif,
regex: /^elseif\s+([^\s].*)$/,
next: [
Twig.logic.type.else_,
Twig.logic.type.elseif,
Twig.logic.type.endif
],
open: false,
compile: function (token) {
var expression = token.match[1]; |
Compile the expression. | token.stack = Twig.expression.compile.apply(this, [{
type: Twig.expression.type.expression,
value: expression
}]).stack;
delete token.match;
return token;
},
parse: function (token, context, chain) {
var output = '';
if (chain && Twig.expression.parse.apply(this, [token.stack, context]) === true) {
chain = false; |
parse if output | output = Twig.parse.apply(this, [token.output, context]);
}
return {
chain: chain,
output: output
};
}
},
{
/**
* Else if type logic tokens.
*
* Format: {% elseif expression %}
*/
type: Twig.logic.type.else_,
regex: /^else$/,
next: [
Twig.logic.type.endif,
Twig.logic.type.endfor
],
open: false,
parse: function (token, context, chain) {
var output = '';
if (chain) {
output = Twig.parse.apply(this, [token.output, context]);
}
return {
chain: chain,
output: output
};
}
},
{
/**
* End if type logic tokens.
*
* Format: {% endif %}
*/
type: Twig.logic.type.endif,
regex: /^endif$/,
next: [ ],
open: false
},
{
/**
* For type logic tokens.
*
* Format: {% for expression %}
*/
type: Twig.logic.type.for_,
regex: /^for\s+([a-zA-Z0-9_,\s]+)\s+in\s+([^\s].*?)(?:\s+if\s+([^\s].*))?$/,
next: [
Twig.logic.type.else_,
Twig.logic.type.endfor
],
open: true,
compile: function (token) {
var key_value = token.match[1],
expression = token.match[2],
conditional = token.match[3],
kv_split = null;
token.key_var = null;
token.value_var = null;
if (key_value.indexOf(",") >= 0) {
kv_split = key_value.split(',');
if (kv_split.length === 2) {
token.key_var = kv_split[0].trim();
token.value_var = kv_split[1].trim();
} else {
throw new Twig.Error("Invalid expression in for loop: " + key_value);
}
} else {
token.value_var = key_value;
} |
Valid expressions for a for loop for item in expression for key,item in expression | |
Compile the expression. | token.expression = Twig.expression.compile.apply(this, [{
type: Twig.expression.type.expression,
value: expression
}]).stack; |
Compile the conditional (if available) | if (conditional) {
token.conditional = Twig.expression.compile.apply(this, [{
type: Twig.expression.type.expression,
value: conditional
}]).stack;
}
delete token.match;
return token;
},
parse: function (token, context, continue_chain) { |
Parse expression | var result = Twig.expression.parse.apply(this, [token.expression, context]),
output = [],
len,
index = 0,
keyset,
that = this,
conditional = token.conditional,
buildLoop = function(index, len) {
var isConditional = conditional !== undefined;
return {
index: index+1,
index0: index,
revindex: isConditional?undefined:len-index,
revindex0: isConditional?undefined:len-index-1,
first: (index === 0),
last: isConditional?undefined:(index === len-1),
length: isConditional?undefined:len,
parent: context
};
},
loop = function(key, value) {
var inner_context = Twig.lib.copy(context);
inner_context[token.value_var] = value;
if (token.key_var) {
inner_context[token.key_var] = key;
} |
Loop object | inner_context.loop = buildLoop(index, len);
if (conditional === undefined ||
Twig.expression.parse.apply(that, [conditional, inner_context]))
{
output.push(Twig.parse.apply(that, [token.output, inner_context]));
index += 1;
}
};
if (result instanceof Array) {
len = result.length;
result.forEach(function (value) {
var key = index;
loop(key, value);
});
} else if (result instanceof Object) {
if (result._keys !== undefined) {
keyset = result._keys;
} else {
keyset = Object.keys(result);
}
len = keyset.length;
keyset.forEach(function(key) { |
Ignore the _keys property, it's internal to twig.js | if (key === "_keys") return;
loop(key, result[key]);
});
} |
Only allow else statements if no output was generated | continue_chain = (output.length === 0);
return {
chain: continue_chain,
output: output.join("")
};
}
},
{
/**
* End if type logic tokens.
*
* Format: {% endif %}
*/
type: Twig.logic.type.endfor,
regex: /^endfor$/,
next: [ ],
open: false
},
{
/**
* Set type logic tokens.
*
* Format: {% set key = expression %}
*/
type: Twig.logic.type.set,
regex: /^set\s+([a-zA-Z0-9_,\s]+)\s*=\s*(.+)$/,
next: [ ],
open: true,
compile: function (token) {
var key = token.match[1].trim(),
expression = token.match[2], |
Compile the expression. | expression_stack = Twig.expression.compile.apply(this, [{
type: Twig.expression.type.expression,
value: expression
}]).stack;
token.key = key;
token.expression = expression_stack;
delete token.match;
return token;
},
parse: function (token, context, continue_chain) {
var value = Twig.expression.parse.apply(this, [token.expression, context]),
key = token.key; |
set on both the global and local context | this.context[key] = value;
context[key] = value;
return {
chain: continue_chain,
context: context
};
}
},
{
/**
* Filter logic tokens.
*
* Format: {% filter upper %} or {% filter lower|escape %}
*/
type: Twig.logic.type.filter,
regex: /^filter\s+(.+)$/,
next: [
Twig.logic.type.endfilter
],
open: true,
compile: function (token) {
var expression = "|" + token.match[1].trim(); |
Compile the expression. | token.stack = Twig.expression.compile.apply(this, [{
type: Twig.expression.type.expression,
value: expression
}]).stack;
delete token.match;
return token;
},
parse: function (token, context, chain) {
var unfiltered = Twig.parse.apply(this, [token.output, context]),
stack = [{
type: Twig.expression.type.string,
value: unfiltered
}].concat(token.stack);
var output = Twig.expression.parse.apply(this, [stack, context]);
return {
chain: chain,
output: output
};
}
},
{
/**
* End filter logic tokens.
*
* Format: {% endfilter %}
*/
type: Twig.logic.type.endfilter,
regex: /^endfilter$/,
next: [ ],
open: false
},
{
/**
* Block logic tokens.
*
* Format: {% block title %}
*/
type: Twig.logic.type.block,
regex: /^block\s+([a-zA-Z0-9_]+)$/,
next: [
Twig.logic.type.endblock
],
open: true,
compile: function (token) {
token.block = token.match[1].trim();
delete token.match;
return token;
},
parse: function (token, context, chain) {
var block_output = "",
output = "",
hasParent = this.blocks[token.block] && this.blocks[token.block].indexOf(Twig.placeholders.parent) > -1; |
Don't override previous blocks | if (this.blocks[token.block] === undefined || hasParent) {
block_output = Twig.expression.parse.apply(this, [{
type: Twig.expression.type.string,
value: Twig.parse.apply(this, [token.output, context])
}, context]);
if (hasParent) {
this.blocks[token.block] = this.blocks[token.block].replace(Twig.placeholders.parent, block_output);
} else {
this.blocks[token.block] = block_output;
}
} |
This is the base template -> append to output | if ( this.extend === null ) { |
Check if a child block has been set from a template extending this one. | if (this.child.blocks[token.block]) {
output = this.child.blocks[token.block];
} else {
output = this.blocks[token.block];
}
}
return {
chain: chain,
output: output
};
}
},
{
/**
* End filter logic tokens.
*
* Format: {% endfilter %}
*/
type: Twig.logic.type.endblock,
regex: /^endblock$/,
next: [ ],
open: false
},
{
/**
* Block logic tokens.
*
* Format: {% extends "template.twig" %}
*/
type: Twig.logic.type.extends_,
regex: /^extends\s+(.+)$/,
next: [ ],
open: true,
compile: function (token) {
var expression = token.match[1].trim();
delete token.match;
token.stack = Twig.expression.compile.apply(this, [{
type: Twig.expression.type.expression,
value: expression
}]).stack;
return token;
},
parse: function (token, context, chain) { |
Resolve filename | var file = Twig.expression.parse.apply(this, [token.stack, context]); |
Set parent template | this.extend = file;
return {
chain: chain,
output: ''
};
}
},
{
/**
* Block logic tokens.
*
* Format: {% extends "template.twig" %}
*/
type: Twig.logic.type.use,
regex: /^use\s+(.+)$/,
next: [ ],
open: true,
compile: function (token) {
var expression = token.match[1].trim();
delete token.match;
token.stack = Twig.expression.compile.apply(this, [{
type: Twig.expression.type.expression,
value: expression
}]).stack;
return token;
},
parse: function (token, context, chain) { |
Resolve filename | var file = Twig.expression.parse.apply(this, [token.stack, context]); |
Import blocks | this.importBlocks(file);
return {
chain: chain,
output: ''
};
}
},
{
/**
* Block logic tokens.
*
* Format: {% includes "template.twig" [with {some: 'values'} only] %}
*/
type: Twig.logic.type.include,
regex: /^include\s+(ignore missing\s+)?(.+?)\s*(?:with\s+(.+?))?\s*(only)?$/,
next: [ ],
open: true,
compile: function (token) {
var match = token.match,
includeMissing = match[1] !== undefined,
expression = match[2].trim(),
withContext = match[3],
only = match[4] !== undefined;
delete token.match;
token.only = only;
token.includeMissing = includeMissing;
token.stack = Twig.expression.compile.apply(this, [{
type: Twig.expression.type.expression,
value: expression
}]).stack;
if (withContext !== undefined) {
token.withStack = Twig.expression.compile.apply(this, [{
type: Twig.expression.type.expression,
value: withContext.trim()
}]).stack;
}
return token;
},
parse: function (token, context, chain) { |
Resolve filename | var innerContext = {},
withContext,
i,
template;
if (!token.only) {
for (i in context) {
if (context.hasOwnProperty(i))
innerContext[i] = context[i];
}
}
if (token.withStack !== undefined) {
withContext = Twig.expression.parse.apply(this, [token.withStack, context]);
for (i in withContext) {
if (withContext.hasOwnProperty(i))
innerContext[i] = withContext[i];
}
}
var file = Twig.expression.parse.apply(this, [token.stack, innerContext]); |
Import file | template = this.importFile(file);
return {
chain: chain,
output: template.render(innerContext)
};
}
}
];
/**
* Registry for logic handlers.
*/
Twig.logic.handler = {};
/**
* Define a new token type, available at Twig.logic.type.{type}
*/
Twig.logic.extendType = function (type, value) {
value = value || ("Twig.logic.type" + type);
Twig.logic.type[type] = value;
};
/**
* Extend the logic parsing functionality with a new token definition.
*
* // Define a new tag
* Twig.logic.extend({
* type: Twig.logic.type.{type},
* // The pattern to match for this token
* regex: ...,
* // What token types can follow this token, leave blank if any.
* next: [ ... ]
* // Create and return compiled version of the token
* compile: function(token) { ... }
* // Parse the compiled token with the context provided by the render call
* // and whether this token chain is complete.
* parse: function(token, context, chain) { ... }
* });
*
* @param {Object} definition The new logic expression.
*/
Twig.logic.extend = function (definition) {
if (!definition.type) {
throw new Twig.Error("Unable to extend logic definition. No type provided for " + definition);
}
if (Twig.logic.type[definition.type]) {
throw new Twig.Error("Unable to extend logic definitions. Type " +
definition.type + " is already defined.");
} else {
Twig.logic.extendType(definition.type);
}
Twig.logic.handler[definition.type] = definition;
}; |
Extend with built-in expressions | while (Twig.logic.definitions.length > 0) {
Twig.logic.extend(Twig.logic.definitions.shift());
}
/**
* Compile a logic token into an object ready for parsing.
*
* @param {Object} raw_token An uncompiled logic token.
*
* @return {Object} A compiled logic token, ready for parsing.
*/
Twig.logic.compile = function (raw_token) {
var expression = raw_token.value.trim(),
token = Twig.logic.tokenize.apply(this, [expression]),
token_template = Twig.logic.handler[token.type]; |
Check if the token needs compiling | if (token_template.compile) {
token = token_template.compile.apply(this, [token]);
Twig.log.trace("Twig.logic.compile: ", "Compiled logic token to ", token);
}
return token;
};
/**
* Tokenize logic expressions. This function matches token expressions against regular
* expressions provided in token definitions provided with Twig.logic.extend.
*
* @param {string} expression the logic token expression to tokenize
* (i.e. what's between {% and %})
*
* @return {Object} The matched token with type set to the token type and match to the regex match.
*/
Twig.logic.tokenize = function (expression) {
var token = {},
token_template_type = null,
token_type = null,
token_regex = null,
regex_array = null,
regex = null,
match = null; |
Ignore whitespace around expressions. | expression = expression.trim();
for (token_template_type in Twig.logic.handler) {
if (Twig.logic.handler.hasOwnProperty(token_template_type)) { |
Get the type and regex for this template type | token_type = Twig.logic.handler[token_template_type].type;
token_regex = Twig.logic.handler[token_template_type].regex; |
Handle multiple regular expressions per type. | regex_array = [];
if (token_regex instanceof Array) {
regex_array = token_regex;
} else {
regex_array.push(token_regex);
} |
Check regular expressions in the order they were specified in the definition. | while (regex_array.length > 0) {
regex = regex_array.shift();
match = regex.exec(expression.trim());
if (match !== null) {
token.type = token_type;
token.match = match;
Twig.log.trace("Twig.logic.tokenize: ", "Matched a ", token_type, " regular expression of ", match);
return token;
}
}
}
} |
No regex matches | throw new Twig.Error("Unable to parse '" + expression.trim() + "'");
};
/**
* Parse a logic token within a given context.
*
* What are logic chains?
* Logic chains represent a series of tokens that are connected,
* for example:
* {% if ... %} {% else %} {% endif %}
*
* The chain parameter is used to signify if a chain is open of closed.
* open:
* More tokens in this chain should be parsed.
* closed:
* This token chain has completed parsing and any additional
* tokens (else, elseif, etc...) should be ignored.
*
* @param {Object} token The compiled token.
* @param {Object} context The render context.
* @param {boolean} chain Is this an open logic chain. If false, that means a
* chain is closed and no further cases should be parsed.
*/
Twig.logic.parse = function (token, context, chain) {
var output = '',
token_template;
context = context || { };
Twig.log.debug("Twig.logic.parse: ", "Parsing logic token ", token);
token_template = Twig.logic.handler[token.type];
if (token_template.parse) {
output = token_template.parse.apply(this, [token, context, chain]);
}
return output;
};
return Twig;
})(Twig || { });
|