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