twig
Version:
JS port of the Twig templating language.
1,412 lines (1,279 loc) • 55.2 kB
JavaScript
// ## twig.logic.js
//
// This file handles tokenizing, compiling and parsing logic tokens. {% ... %}
module.exports = 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',
apply: 'Twig.logic.type.apply',
endapply: 'Twig.logic.type.endapply',
do: 'Twig.logic.type.do',
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',
with: 'Twig.logic.type.with',
endwith: 'Twig.logic.type.endwith',
deprecated: 'Twig.logic.type.deprecated'
};
// 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_,
regex: /^if\s?([\s\S]+)$/,
next: [
Twig.logic.type.else_,
Twig.logic.type.elseif,
Twig.logic.type.endif
],
open: true,
compile(token) {
const expression = token.match[1];
// Compile the expression.
token.stack = Twig.expression.compile.call(this, {
type: Twig.expression.type.expression,
value: expression
}).stack;
delete token.match;
return token;
},
parse(token, context, chain) {
const state = this;
return Twig.expression.parseAsync.call(state, token.stack, context)
.then(result => {
chain = true;
if (Twig.lib.boolval(result)) {
chain = false;
return state.parseAsync(token.output, context);
}
return '';
})
.then(output => {
return {
chain,
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(token) {
const expression = token.match[1];
// Compile the expression.
token.stack = Twig.expression.compile.call(this, {
type: Twig.expression.type.expression,
value: expression
}).stack;
delete token.match;
return token;
},
parse(token, context, chain) {
const state = this;
return Twig.expression.parseAsync.call(state, token.stack, context)
.then(result => {
if (chain && Twig.lib.boolval(result)) {
chain = false;
return state.parseAsync(token.output, context);
}
return '';
})
.then(output => {
return {
chain,
output
};
});
}
},
{
/**
* Else type logic tokens.
*
* Format: {% else %}
*/
type: Twig.logic.type.else_,
regex: /^else$/,
next: [
Twig.logic.type.endif,
Twig.logic.type.endfor
],
open: false,
parse(token, context, chain) {
let promise = Twig.Promise.resolve('');
const state = this;
if (chain) {
promise = state.parseAsync(token.output, context);
}
return promise.then(output => {
return {
chain,
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]+?)(?:\s+if\s+([^\s].*))?$/,
next: [
Twig.logic.type.else_,
Twig.logic.type.endfor
],
open: true,
compile(token) {
const keyValue = token.match[1];
const expression = token.match[2];
const conditional = token.match[3];
let kvSplit = null;
token.keyVar = null;
token.valueVar = null;
if (keyValue.includes(',')) {
kvSplit = keyValue.split(',');
if (kvSplit.length === 2) {
token.keyVar = kvSplit[0].trim();
token.valueVar = kvSplit[1].trim();
} else {
throw new Twig.Error('Invalid expression in for loop: ' + keyValue);
}
} else {
token.valueVar = keyValue.trim();
}
// Valid expressions for a for loop
// for item in expression
// for key,item in expression
// Compile the expression.
token.expression = Twig.expression.compile.call(this, {
type: Twig.expression.type.expression,
value: expression
}).stack;
// Compile the conditional (if available)
if (conditional) {
token.conditional = Twig.expression.compile.call(this, {
type: Twig.expression.type.expression,
value: conditional
}).stack;
}
delete token.match;
return token;
},
parse(token, context, continueChain) {
// Parse expression
const output = [];
let len;
let index = 0;
let keyset;
const state = this;
const {conditional} = token;
const buildLoop = function (index, len) {
const 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
const loop = function (key, value) {
const innerContext = {...context};
innerContext[token.valueVar] = value;
if (token.keyVar) {
innerContext[token.keyVar] = key;
}
// Loop object
innerContext.loop = buildLoop(index, len);
const promise = conditional === undefined ?
Twig.Promise.resolve(true) :
Twig.expression.parseAsync.call(state, conditional, innerContext);
return promise.then(condition => {
if (!condition) {
return;
}
return state.parseAsync(token.output, innerContext)
.then(tokenOutput => {
output.push(tokenOutput);
index += 1;
});
})
.then(() => {
// Delete loop-related variables from the context
delete innerContext.loop;
delete innerContext[token.valueVar];
delete innerContext[token.keyVar];
// Merge in values that exist in context but have changed
// in inner_context.
Twig.merge(context, innerContext, true);
});
};
return Twig.expression.parseAsync.call(state, token.expression, context)
.then(result => {
if (Array.isArray(result)) {
len = result.length;
return Twig.async.forEach(result, value => {
const key = index;
return loop(key, value);
});
}
if (Twig.lib.is('Object', result)) {
if (result._keys === undefined) {
keyset = Object.keys(result);
} else {
keyset = result._keys;
}
len = keyset.length;
return Twig.async.forEach(keyset, key => {
// Ignore the _keys property, it's internal to twig.js
if (key === '_keys') {
return;
}
return loop(key, result[key]);
});
}
})
.then(() => {
// Only allow else statements if no output was generated
continueChain = (output.length === 0);
return {
chain: continueChain,
context,
output: Twig.output.call(state.template, output)
};
});
}
},
{
/**
* End for type logic tokens.
*
* Format: {% endfor %}
*/
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*([\s\S]+)$/,
next: [],
open: true,
compile(token) { //
const key = token.match[1].trim();
const expression = token.match[2];
// Compile the expression.
const expressionStack = Twig.expression.compile.call(this, {
type: Twig.expression.type.expression,
value: expression
}).stack;
token.key = key;
token.expression = expressionStack;
delete token.match;
return token;
},
parse(token, context, continueChain) {
const {key} = token;
const state = this;
return Twig.expression.parseAsync.call(state, token.expression, context)
.then(value => {
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 = {...value};
}
context[key] = value;
return {
chain: continueChain,
context
};
});
}
},
{
/**
* Set capture type logic tokens.
*
* Format: {% set key %}
*/
type: Twig.logic.type.setcapture,
regex: /^set\s+([a-zA-Z0-9_,\s]+)$/,
next: [
Twig.logic.type.endset
],
open: true,
compile(token) {
const key = token.match[1].trim();
token.key = key;
delete token.match;
return token;
},
parse(token, context, continueChain) {
const state = this;
const {key} = token;
return state.parseAsync(token.output, context)
.then(output => {
// Set on both the global and local context
state.context[key] = output;
context[key] = output;
return {
chain: continueChain,
context
};
});
}
},
{
/**
* End set type block logic tokens.
*
* Format: {% endset %}
*/
type: Twig.logic.type.endset,
regex: /^endset$/,
next: [],
open: false
},
{
/**
* 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(token) {
const expression = '|' + token.match[1].trim();
// Compile the expression.
token.stack = Twig.expression.compile.call(this, {
type: Twig.expression.type.expression,
value: expression
}).stack;
delete token.match;
return token;
},
parse(token, context, chain) {
const state = this;
return state.parseAsync(token.output, context)
.then(output => {
const stack = [{
type: Twig.expression.type.string,
value: output
}].concat(token.stack);
return Twig.expression.parseAsync.call(state, stack, context);
})
.then(output => {
return {
chain,
output
};
});
}
},
{
/**
* End filter logic tokens.
*
* Format: {% endfilter %}
*/
type: Twig.logic.type.endfilter,
regex: /^endfilter$/,
next: [],
open: false
},
{
/**
* Apply logic tokens.
*
* Format: {% apply upper %} or {% apply lower|escape %}
*/
type: Twig.logic.type.apply,
regex: /^apply\s+(.+)$/,
next: [
Twig.logic.type.endapply
],
open: true,
compile(token) {
const expression = '|' + token.match[1].trim();
// Compile the expression.
token.stack = Twig.expression.compile.call(this, {
type: Twig.expression.type.expression,
value: expression
}).stack;
delete token.match;
return token;
},
parse(token, context, chain) {
const state = this;
return state.parseAsync(token.output, context)
.then(output => {
const stack = [{
type: Twig.expression.type.string,
value: output
}].concat(token.stack);
return Twig.expression.parseAsync.call(state, stack, context);
})
.then(output => {
return {
chain,
output
};
});
}
},
{
/**
* End apply logic tokens.
*
* Format: {% endapply %}
*/
type: Twig.logic.type.endapply,
regex: /^endapply$/,
next: [],
open: false
},
{
/**
* Set type logic tokens.
*
* Format: {% do expression %}
*/
type: Twig.logic.type.do,
regex: /^do\s+([\S\s]+)$/,
next: [],
open: true,
compile(token) { //
const expression = token.match[1];
// Compile the expression.
const expressionStack = Twig.expression.compile.call(this, {
type: Twig.expression.type.expression,
value: expression
}).stack;
token.expression = expressionStack;
delete token.match;
return token;
},
parse(token, context, continueChain) {
const state = this;
return Twig.expression.parseAsync.call(state, token.expression, context)
.then(() => {
return {
chain: continueChain,
context
};
});
}
},
{
/**
* Block logic tokens.
*
* Format: {% block title %}
*/
type: Twig.logic.type.block,
regex: /^block\s+(\w+)$/,
next: [
Twig.logic.type.endblock
],
open: true,
compile(token) {
token.blockName = token.match[1].trim();
delete token.match;
return token;
},
parse(token, context, chain) {
const state = this;
let promise = Twig.Promise.resolve();
state.template.blocks.defined[token.blockName] = new Twig.Block(state.template, token);
if (
state.template.parentTemplate === null ||
state.template.parentTemplate instanceof Twig.Template
) {
promise = state.getBlock(token.blockName).render(state, context);
}
return promise.then(output => {
return {
chain,
output
};
});
}
},
{
/**
* Block shorthand logic tokens.
*
* Format: {% block title expression %}
*/
type: Twig.logic.type.shortblock,
regex: /^block\s+(\w+)\s+(.+)$/,
next: [],
open: true,
compile(token) {
const template = this;
token.expression = token.match[2].trim();
token.output = Twig.expression.compile({
type: Twig.expression.type.expression,
value: token.expression
}).stack;
return Twig.logic.handler[Twig.logic.type.block].compile.apply(template, [token]);
},
parse(...args) {
const state = this;
return Twig.logic.handler[Twig.logic.type.block].parse.apply(state, args);
}
},
{
/**
* End block logic tokens.
*
* Format: {% endblock %}
*/
type: Twig.logic.type.endblock,
regex: /^endblock(?:\s+(\w+))?$/,
next: [],
open: false
},
{
/**
* Block logic tokens.
*
* Format: {% extends "template.twig" %}
*/
type: Twig.logic.type.extends_,
regex: /^extends\s+(.+)$/,
next: [],
open: true,
compile(token) {
const expression = token.match[1].trim();
delete token.match;
token.stack = Twig.expression.compile.call(this, {
type: Twig.expression.type.expression,
value: expression
}).stack;
return token;
},
parse(token, context, chain) {
const state = this;
return Twig.expression.parseAsync.call(state, token.stack, context)
.then(fileName => {
if (Array.isArray(fileName)) {
const result = fileName.reverse().reduce((acc, file) => {
try {
return {
render: state.template.importFile(file),
fileName: file
};
} catch (error) {
return acc;
}
}, {
render: null,
fileName: null
});
if (result.fileName !== null) {
state.template.parentTemplate = result.fileName;
}
} else {
state.template.parentTemplate = fileName;
}
return {
chain,
output: ''
};
});
}
},
{
/**
* Block logic tokens.
*
* Format: {% use "template.twig" %}
*/
type: Twig.logic.type.use,
regex: /^use\s+(.+)$/,
next: [],
open: true,
compile(token) {
const expression = token.match[1].trim();
delete token.match;
token.stack = Twig.expression.compile.call(this, {
type: Twig.expression.type.expression,
value: expression
}).stack;
return token;
},
parse(token, context, chain) {
const state = this;
return Twig.expression.parseAsync.call(state, token.stack, context)
.then(filePath => {
// Create a new state instead of using the current state
// any defined blocks will be created in isolation
const useTemplate = state.template.importFile(filePath);
const useState = new Twig.ParseState(useTemplate);
return useState.parseAsync(useTemplate.tokens)
.then(() => {
state.template.blocks.imported = {
...state.template.blocks.imported,
...useState.getBlocks()
};
});
})
.then(() => {
return {
chain,
output: ''
};
});
}
},
{
/**
* Block logic tokens.
*
* Format: {% includes "template.twig" [with {some: 'values'} only] %}
*/
type: Twig.logic.type.include,
regex: /^include\s+(.+?)(?:\s|$)(ignore missing(?:\s|$))?(?:with\s+([\S\s]+?))?(?:\s|$)(only)?$/,
next: [],
open: true,
compile(token) {
const {match} = token;
const expression = match[1].trim();
const ignoreMissing = match[2] !== undefined;
const withContext = match[3];
const only = ((match[4] !== undefined) && match[4].length);
delete token.match;
token.only = only;
token.ignoreMissing = ignoreMissing;
token.stack = Twig.expression.compile.call(this, {
type: Twig.expression.type.expression,
value: expression
}).stack;
if (withContext !== undefined) {
token.withStack = Twig.expression.compile.call(this, {
type: Twig.expression.type.expression,
value: withContext.trim()
}).stack;
}
return token;
},
parse(token, context, chain) {
// Resolve filename
let innerContext = token.only ? {} : {...context};
const {ignoreMissing} = token;
const state = this;
let promise = null;
const result = {chain, output: ''};
if (typeof token.withStack === 'undefined') {
promise = Twig.Promise.resolve();
} else {
promise = Twig.expression.parseAsync.call(state, token.withStack, context)
.then(withContext => {
innerContext = {
...innerContext,
...withContext
};
});
}
return promise
.then(() => {
return Twig.expression.parseAsync.call(state, token.stack, context);
})
.then(file => {
let files;
if (Array.isArray(file)) {
files = file;
} else {
files = [file];
}
const result = files.reduce((acc, file) => {
if (acc.render === null) {
if (file instanceof Twig.Template) {
return {
render: file.renderAsync(
innerContext,
{
isInclude: true
}
),
lastError: null
};
}
try {
return {
render: state.template.importFile(file).renderAsync(
innerContext,
{
isInclude: true
}
),
lastError: null
};
} catch (error) {
return {
render: null,
lastError: error
};
}
}
return acc;
}, {render: null, lastError: null});
if (result.render !== null) {
return result.render;
}
if (result.render === null && ignoreMissing) {
return '';
}
throw result.lastError;
})
.then(output => {
if (output !== '') {
result.output = output;
}
return result;
});
}
},
{
type: Twig.logic.type.spaceless,
regex: /^spaceless$/,
next: [
Twig.logic.type.endspaceless
],
open: true,
// Parse the html and return it without any spaces between tags
parse(token, context, chain) {
const state = this;
// Parse the output without any filter
return state.parseAsync(token.output, context)
.then(tokenOutput => {
const // 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
let output = tokenOutput.replace(rBetweenTagSpaces, '><').trim();
// Rewrap output as a Twig.Markup
output = new Twig.Markup(output);
return {
chain,
output
};
});
}
},
// Add the {% endspaceless %} token
{
type: Twig.logic.type.endspaceless,
regex: /^endspaceless$/,
next: [],
open: false
},
{
/**
* Macro logic tokens.
*
* Format: {% macro input(name = default, value, type, size) %}
*
*/
type: Twig.logic.type.macro,
regex: /^macro\s+(\w+)\s*\(\s*((?:\w+(?:\s*=\s*([\s\S]+))?(?:,\s*)?)*)\s*\)$/,
next: [
Twig.logic.type.endmacro
],
open: true,
compile(token) {
const macroName = token.match[1];
const rawParameters = token.match[2].split(/\s*,\s*/);
const parameters = rawParameters.map(rawParameter => {
return rawParameter.split(/\s*=\s*/)[0];
});
const parametersCount = parameters.length;
// Duplicate check
if (parametersCount > 1) {
const uniq = {};
for (let i = 0; i < parametersCount; i++) {
const parameter = parameters[i];
if (uniq[parameter]) {
throw new Twig.Error('Duplicate arguments for parameter: ' + parameter);
} else {
uniq[parameter] = 1;
}
}
}
token.macroName = macroName;
token.parameters = parameters;
token.defaults = rawParameters.reduce(function (defaults, rawParameter) {
const pair = rawParameter.split(/\s*=\s*/);
const key = pair[0];
const expression = pair[1];
if (expression) {
defaults[key] = Twig.expression.compile.call(this, {
type: Twig.expression.type.expression,
value: expression
}).stack;
} else {
defaults[key] = undefined;
}
return defaults;
}, {});
delete token.match;
return token;
},
parse(token, context, chain) {
const state = this;
state.macros[token.macroName] = function (...args) {
// Pass global context and other macros
const macroContext = {
// Use current state context because state context includes current loop variables as well
...state.context,
_self: state.macros
};
// Save arguments
return Twig.async.forEach(token.parameters, function (prop, i) {
// Add parameters from context to macroContext
if (typeof args[i] !== 'undefined') {
macroContext[prop] = args[i];
return true;
}
if (typeof token.defaults[prop] !== 'undefined') {
return Twig.expression.parseAsync.call(this, token.defaults[prop], context)
.then(value => {
macroContext[prop] = value;
return Twig.Promise.resolve();
});
}
macroContext[prop] = undefined;
return true;
}).then(() => {
// Render
return state.parseAsync(token.output, macroContext);
});
};
return {
chain,
output: ''
};
}
},
{
/**
* End macro logic tokens.
*
* Format: {% endmacro %}
*/
type: Twig.logic.type.endmacro,
regex: /^endmacro$/,
next: [],
open: false
},
{
/*
* Import logic tokens.
*
* Format: {% import "template.twig" as form %}
*/
type: Twig.logic.type.import_,
regex: /^import\s+(.+)\s+as\s+(\w+)$/,
next: [],
open: true,
compile(token) {
const expression = token.match[1].trim();
const contextName = token.match[2].trim();
delete token.match;
token.expression = expression;
token.contextName = contextName;
token.stack = Twig.expression.compile.call(this, {
type: Twig.expression.type.expression,
value: expression
}).stack;
return token;
},
parse(token, context, chain) {
const state = this;
const output = {
chain,
output: ''
};
if (token.expression === '_self') {
context[token.contextName] = state.macros;
return output;
}
return Twig.expression.parseAsync.call(state, token.stack, context)
.then(filePath => {
return state.template.importFile(filePath || token.expression);
})
.then(importTemplate => {
const importState = new Twig.ParseState(importTemplate);
return importState.parseAsync(importTemplate.tokens).then(() => {
context[token.contextName] = importState.macros;
return output;
});
});
}
},
{
/*
* From logic tokens.
*
* Format: {% from "template.twig" import func as form %}
*/
type: Twig.logic.type.from,
regex: /^from\s+(.+)\s+import\s+([a-zA-Z0-9_, ]+)$/,
next: [],
open: true,
compile(token) {
const expression = token.match[1].trim();
const macroExpressions = token.match[2].trim().split(/\s*,\s*/);
const macroNames = {};
for (const res of macroExpressions) {
// Match function as variable
const macroMatch = res.match(/^(\w+)\s+as\s+(\w+)$/);
if (macroMatch) {
macroNames[macroMatch[1].trim()] = macroMatch[2].trim();
} else if (res.match(/^(\w+)$/)) {
macroNames[res] = res;
} else {
// ignore import
}
}
delete token.match;
token.expression = expression;
token.macroNames = macroNames;
token.stack = Twig.expression.compile.call(this, {
type: Twig.expression.type.expression,
value: expression
}).stack;
return token;
},
parse(token, context, chain) {
const state = this;
let promise;
if (token.expression === '_self') {
promise = Twig.Promise.resolve(state.macros);
} else {
promise = Twig.expression.parseAsync.call(state, token.stack, context)
.then(filePath => {
return state.template.importFile(filePath || token.expression);
})
.then(importTemplate => {
const importState = new Twig.ParseState(importTemplate);
return importState.parseAsync(importTemplate.tokens).then(() => {
return importState.macros;
});
});
}
return promise
.then(macros => {
for (const macroName in token.macroNames) {
if (macros[macroName] !== undefined) {
context[token.macroNames[macroName]] = macros[macroName];
}
}
return {
chain,
output: ''
};
});
}
},
{
/**
* 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,
regex: /^embed\s+(.+?)(?:\s+(ignore missing))?(?:\s+with\s+([\S\s]+?))?(?:\s+(only))?$/,
next: [
Twig.logic.type.endembed
],
open: true,
compile(token) {
const {match} = token;
const expression = match[1].trim();
const ignoreMissing = match[2] !== undefined;
const withContext = match[3];
const only = ((match[4] !== undefined) && match[4].length);
delete token.match;
token.only = only;
token.ignoreMissing = ignoreMissing;
token.stack = Twig.expression.compile.call(this, {
type: Twig.expression.type.expression,
value: expression
}).stack;
if (withContext !== undefined) {
token.withStack = Twig.expression.compile.call(this, {
type: Twig.expression.type.expression,
value: withContext.trim()
}).stack;
}
return token;
},
parse(token, context, chain) {
let embedContext = {};
let promise = Twig.Promise.resolve();
let state = this;
if (!token.only) {
embedContext = {...context};
}
if (token.withStack !== undefined) {
promise = Twig.expression.parseAsync.call(state, token.withStack, context).then(withContext => {
embedContext = {...embedContext, ...withContext};
});
}
return promise
.then(() => {
return Twig.expression.parseAsync.call(state, token.stack, embedContext);
})
.then(fileName => {
const embedOverrideTemplate = new Twig.Template({
data: token.output,
base: state.template.base,
path: state.template.path,
url: state.template.url,
name: state.template.name,
method: state.template.method,
options: state.template.options
});
try {
embedOverrideTemplate.importFile(fileName);
} catch (error) {
if (token.ignoreMissing) {
return '';
}
// Errors preserve references to variables in scope,
// this removes `this` from the scope.
state = null;
throw error;
}
embedOverrideTemplate.parentTemplate = fileName;
return embedOverrideTemplate.renderAsync(
embedContext,
{
isInclude: true
}
);
})
.then(output => {
return {
chain,
output
};
});
}
},
/* Add the {% endembed %} token
*
*/
{
type: Twig.logic.type.endembed,
regex: /^endembed$/,
next: [],
open: false
},
{
/**
* Block logic tokens.
*
* Format: {% with {some: 'values'} [only] %}
*/
type: Twig.logic.type.with,
regex: /^(?:with(?:\s+([\S\s]+?))?)(?:\s|$)(only)?$/,
next: [
Twig.logic.type.endwith
],
open: true,
compile(token) {
const {match} = token;
const withContext = match[1];
const only = ((match[2] !== undefined) && match[2].length);
delete token.match;
token.only = only;
if (withContext !== undefined) {
token.withStack = Twig.expression.compile.call(this, {
type: Twig.expression.type.expression,
value: withContext.trim()
}).stack;
}
return token;
},
parse(token, context, chain) {
// Resolve filename
let innerContext = {};
let i;
const state = this;
let promise = Twig.Promise.resolve();
if (!token.only) {
innerContext = {...context};
}
if (token.withStack !== undefined) {
promise = Twig.expression.parseAsync.call(state, token.withStack, context)
.then(withContext => {
for (i in withContext) {
if (Object.hasOwnProperty.call(withContext, i)) {
innerContext[i] = withContext[i];
}
}
});
}
const isolatedState = new Twig.ParseState(state.template, undefined, innerContext);
return promise
.then(() => {
return isolatedState.parseAsync(token.output);
})
.then(output => {
return {
chain,
output
};
});
}
},
{
type: Twig.logic.type.endwith,
regex: /^endwith$/,
next: [],
open: false
},
{
/**
* Deprecated type logic tokens.
*
* Format: {% deprecated 'Description' %}
*/
type: Twig.logic.type.deprecated,
regex: /^deprecated\s+(.+)$/,
next: [],
open: true,
compile(token) {
console.warn('Deprecation notice: ' + token.match[1]);
return token;
},
parse() {
return {};
}
}
];
/**
* 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) { ... }
* });
*