twig
Version:
JS port of the Twig templating language.
1,541 lines (1,446 loc) • 291 kB
JavaScript
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else if(typeof exports === 'object')
exports["Twig"] = factory();
else
root["Twig"] = factory();
})(global, () => {
return /******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ 683
(module) {
"use strict";
// ## twig.async.js
//
// This file handles asynchronous tasks within twig.
module.exports = function (Twig) {
'use strict';
var STATE_UNKNOWN = 0;
var STATE_RESOLVED = 1;
var STATE_REJECTED = 2;
Twig.ParseState.prototype.parseAsync = function (tokens, context) {
return this.parse(tokens, context, true);
};
Twig.expression.parseAsync = function (tokens, context, tokensAreParameters) {
var state = this;
return Twig.expression.parse.call(state, tokens, context, tokensAreParameters, true);
};
Twig.logic.parseAsync = function (token, context, chain) {
var state = this;
return Twig.logic.parse.call(state, token, context, chain, true);
};
Twig.Template.prototype.renderAsync = function (context, params) {
return this.render(context, params, true);
};
Twig.async = {};
/**
* Checks for `thenable` objects
*/
Twig.isPromise = function (obj) {
return obj && obj.then && typeof obj.then === 'function';
};
/**
* Handling of code paths that might either return a promise
* or a value depending on whether async code is used.
*
* @see https://github.com/twigjs/twig.js/blob/master/ASYNC.md#detecting-asynchronous-behaviour
*/
function potentiallyAsyncSlow(that, allowAsync, action) {
var result = action.call(that);
var err = null;
var isAsync = true;
if (!Twig.isPromise(result)) {
return result;
}
result.then(function (res) {
result = res;
isAsync = false;
})["catch"](function (error) {
err = error;
});
if (err !== null) {
throw err;
}
if (isAsync) {
throw new Twig.Error('You are using Twig.js in sync mode in combination with async extensions.');
}
return result;
}
Twig.async.potentiallyAsync = function (that, allowAsync, action) {
if (allowAsync) {
return Twig.Promise.resolve(action.call(that));
}
return potentiallyAsyncSlow(that, allowAsync, action);
};
function run(fn, resolve, reject) {
try {
fn(resolve, reject);
} catch (error) {
reject(error);
}
}
function pending(handlers, onResolved, onRejected) {
var h = [onResolved, onRejected, -2];
// The promise has yet to be rejected or resolved.
if (!handlers) {
handlers = h;
} else if (handlers[2] === -2) {
// Only allocate an array when there are multiple handlers
handlers = [handlers, h];
} else {
handlers.push(h);
}
return handlers;
}
/**
* Really small thenable to represent promises that resolve immediately.
*
*/
Twig.Thenable = function (then, value, state) {
this.then = then;
this._value = state ? value : null;
this._state = state || STATE_UNKNOWN;
};
Twig.Thenable.prototype["catch"] = function (onRejected) {
// THe promise will not throw, it has already resolved.
if (this._state === STATE_RESOLVED) {
return this;
}
return this.then(null, onRejected);
};
/**
* The `then` method attached to a Thenable when it has resolved.
*
*/
Twig.Thenable.resolvedThen = function (onResolved) {
try {
return Twig.Promise.resolve(onResolved(this._value));
} catch (error) {
return Twig.Promise.reject(error);
}
};
/**
* The `then` method attached to a Thenable when it has rejected.
*
*/
Twig.Thenable.rejectedThen = function (onResolved, onRejected) {
// Shortcut for rejected twig promises
if (!onRejected || typeof onRejected !== 'function') {
return this;
}
var value = this._value;
var result;
try {
result = onRejected(value);
} catch (error) {
result = Twig.Promise.reject(error);
}
return Twig.Promise.resolve(result);
};
/**
* An alternate implementation of a Promise that does not fully follow
* the spec, but instead works fully synchronous while still being
* thenable.
*
* These promises can be mixed with regular promises at which point
* the synchronous behaviour is lost.
*/
Twig.Promise = function (executor) {
var state = STATE_UNKNOWN;
var value = null;
var changeState = function changeState(nextState, nextValue) {
state = nextState;
value = nextValue;
};
function onReady(v) {
changeState(STATE_RESOLVED, v);
}
function onReject(e) {
changeState(STATE_REJECTED, e);
}
run(executor, onReady, onReject);
// If the promise settles right after running the executor we can
// return a Promise with it's state already set.
//
// Twig.Promise.resolve and Twig.Promise.reject both use the more
// efficient `Twig.Thenable` for this purpose.
if (state === STATE_RESOLVED) {
return Twig.Promise.resolve(value);
}
if (state === STATE_REJECTED) {
return Twig.Promise.reject(value);
}
// If we managed to get here our promise is going to resolve asynchronous.
changeState = new Twig.FullPromise();
return changeState.promise;
};
/**
* Promise implementation that can handle being resolved at any later time.
*
*/
Twig.FullPromise = function () {
var handlers = null;
// The state has been changed to either resolve, or reject
// which means we should call the handler.
function resolved(onResolved) {
onResolved(p._value);
}
function rejected(onResolved, onRejected) {
onRejected(p._value);
}
var append = function append(onResolved, onRejected) {
handlers = pending(handlers, onResolved, onRejected);
};
function changeState(newState, v) {
if (p._state) {
return;
}
p._value = v;
p._state = newState;
append = newState === STATE_RESOLVED ? resolved : rejected;
if (!handlers) {
return;
}
if (handlers[2] === -2) {
append(handlers[0], handlers[1]);
handlers = null;
return;
}
handlers.forEach(function (h) {
append(h[0], h[1]);
});
handlers = null;
}
var p = new Twig.Thenable(function (onResolved, onRejected) {
var hasResolved = typeof onResolved === 'function';
// Shortcut for resolved twig promises
if (p._state === STATE_RESOLVED && !hasResolved) {
return Twig.Promise.resolve(p._value);
}
if (p._state === STATE_RESOLVED) {
try {
return Twig.Promise.resolve(onResolved(p._value));
} catch (error) {
return Twig.Promise.reject(error);
}
}
var hasRejected = typeof onRejected === 'function';
return new Twig.Promise(function (resolve, reject) {
append(hasResolved ? function (result) {
try {
resolve(onResolved(result));
} catch (error) {
reject(error);
}
} : resolve, hasRejected ? function (err) {
try {
resolve(onRejected(err));
} catch (error) {
reject(error);
}
} : reject);
});
});
changeState.promise = p;
return changeState;
};
Twig.Promise.defaultResolved = new Twig.Thenable(Twig.Thenable.resolvedThen, undefined, STATE_RESOLVED);
Twig.Promise.emptyStringResolved = new Twig.Thenable(Twig.Thenable.resolvedThen, '', STATE_RESOLVED);
Twig.Promise.resolve = function (value) {
if (arguments.length === 0 || typeof value === 'undefined') {
return Twig.Promise.defaultResolved;
}
if (Twig.isPromise(value)) {
return value;
}
// Twig often resolves with an empty string, we optimize for this
// scenario by returning a fixed promise. This reduces the load on
// garbage collection.
if (value === '') {
return Twig.Promise.emptyStringResolved;
}
return new Twig.Thenable(Twig.Thenable.resolvedThen, value, STATE_RESOLVED);
};
Twig.Promise.reject = function (e) {
// `e` should never be a promise.
return new Twig.Thenable(Twig.Thenable.rejectedThen, e, STATE_REJECTED);
};
Twig.Promise.all = function (promises) {
var results = new Array(promises.length);
return Twig.async.forEach(promises, function (p, index) {
if (!Twig.isPromise(p)) {
results[index] = p;
return;
}
if (p._state === STATE_RESOLVED) {
results[index] = p._value;
return;
}
return p.then(function (v) {
results[index] = v;
});
}).then(function () {
return results;
});
};
/**
* Go over each item in a fashion compatible with Twig.forEach,
* allow the function to return a promise or call the third argument
* to signal it is finished.
*
* Each item in the array will be called sequentially.
*/
Twig.async.forEach = function (arr, callback) {
var len = arr ? arr.length : 0;
var index = 0;
function next() {
var resp = null;
do {
if (index === len) {
return Twig.Promise.resolve();
}
resp = callback(arr[index], index);
index++;
// While the result of the callback is not a promise or it is
// a promise that has settled we can use a regular loop which
// is much faster.
} while (!resp || !Twig.isPromise(resp) || resp._state === STATE_RESOLVED);
return resp.then(next);
}
return next();
};
return Twig;
};
/***/ },
/***/ 198
(module) {
"use strict";
// ## twig.compiler.js
//
// This file handles compiling templates into JS
module.exports = function (Twig) {
/**
* Namespace for compilation.
*/
Twig.compiler = {
module: {}
};
// Compile a Twig Template to output.
Twig.compiler.compile = function (template, options) {
// Get tokens
var tokens = JSON.stringify(template.tokens);
var id = template.id;
var output = null;
if (options.module) {
if (Twig.compiler.module[options.module] === undefined) {
throw new Twig.Error('Unable to find module type ' + options.module);
}
output = Twig.compiler.module[options.module](id, tokens, options.twig);
} else {
output = Twig.compiler.wrap(id, tokens);
}
return output;
};
Twig.compiler.module = {
amd: function amd(id, tokens, pathToTwig) {
return 'define(["' + pathToTwig + '"], function (Twig) {\n\tvar twig, templates;\ntwig = Twig.twig;\ntemplates = ' + Twig.compiler.wrap(id, tokens) + '\n\treturn templates;\n});';
},
node: function node(id, tokens) {
return 'var twig = require("twig").twig;\nexports.template = ' + Twig.compiler.wrap(id, tokens);
},
cjs2: function cjs2(id, tokens, pathToTwig) {
return 'module.declare([{ twig: "' + pathToTwig + '" }], function (require, exports, module) {\n\tvar twig = require("twig").twig;\n\texports.template = ' + Twig.compiler.wrap(id, tokens) + '\n});';
}
};
Twig.compiler.wrap = function (id, tokens) {
return 'twig({id:"' + id.replace('"', '\\"') + '", data:' + tokens + ', precompiled: true});\n';
};
return Twig;
};
/***/ },
/***/ 636
(module, __unused_webpack_exports, __webpack_require__) {
"use strict";
var _interopRequireDefault = __webpack_require__(994);
var _defineProperty2 = _interopRequireDefault(__webpack_require__(693));
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2["default"])(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
// ## twig.core.js
//
// This file handles template level tokenizing, compiling and parsing.
module.exports = function (Twig) {
'use strict';
Twig.trace = false;
Twig.debug = false;
// Default caching to true for the improved performance it offers
Twig.cache = true;
Twig.noop = function () {};
Twig.merge = function (target, source, onlyChanged) {
Object.keys(source).forEach(function (key) {
if (onlyChanged && !(key in target)) {
return;
}
target[key] = source[key];
});
return target;
};
/**
* Exception thrown by twig.js.
*/
Twig.Error = function (message, file) {
this.message = message;
this.name = 'TwigException';
this.type = 'TwigException';
this.file = file;
};
/**
* 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 trace() {
if (Twig.trace && console) {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
console.log(Array.prototype.slice.call(args));
}
},
debug: function debug() {
if (Twig.debug && console) {
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
console.log(Array.prototype.slice.call(args));
}
}
};
if (typeof console === 'undefined') {
Twig.log.error = function () {};
} else if (typeof console.error !== 'undefined') {
Twig.log.error = function () {
var _console;
(_console = console).error.apply(_console, arguments);
};
} else if (typeof console.log !== 'undefined') {
Twig.log.error = function () {
var _console2;
(_console2 = console).log.apply(_console2, arguments);
};
}
/**
* 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',
outputWhitespacePre: 'output_whitespace_pre',
outputWhitespacePost: 'output_whitespace_post',
outputWhitespaceBoth: 'output_whitespace_both',
logicWhitespacePre: 'logic_whitespace_pre',
logicWhitespacePost: 'logic_whitespace_post',
logicWhitespaceBoth: 'logic_whitespace_both'
};
/**
* Token syntax definitions.
*/
Twig.token.definitions = [{
type: Twig.token.type.raw,
open: '{% raw %}',
close: '{% endraw %}'
}, {
type: Twig.token.type.raw,
open: '{% verbatim %}',
close: '{% endverbatim %}'
},
// *Whitespace type tokens*
//
// These typically take the form `{{- expression -}}` or `{{- expression }}` or `{{ expression -}}`.
{
type: Twig.token.type.outputWhitespacePre,
open: '{{-',
close: '}}'
}, {
type: Twig.token.type.outputWhitespacePost,
open: '{{',
close: '-}}'
}, {
type: Twig.token.type.outputWhitespaceBoth,
open: '{{-',
close: '-}}'
}, {
type: Twig.token.type.logicWhitespacePre,
open: '{%-',
close: '%}'
}, {
type: Twig.token.type.logicWhitespacePost,
open: '{%',
close: '-%}'
}, {
type: Twig.token.type.logicWhitespaceBoth,
open: '{%-',
close: '-%}'
},
// *Output type tokens*
//
// These typically take the form `{{ expression }}`.
{
type: Twig.token.type.output,
open: '{{',
close: '}}'
},
// *Logic type tokens*
//
// These typically take a form like `{% if expression %}` or `{% endif %}`
{
type: Twig.token.type.logic,
open: '{%',
close: '%}'
},
// *Comment type tokens*
//
// These take the form `{# anything #}`
{
type: Twig.token.type.comment,
open: '{#',
close: '#}'
}];
/**
* What characters start "strings" in token definitions. We need this to ignore token close
* strings inside an expression.
*/
Twig.token.strings = ['"', '\''];
Twig.token.findStart = function (template) {
var output = {
position: null,
def: null
};
var closePosition = null;
var len = Twig.token.definitions.length;
var i;
var tokenTemplate;
var firstKeyPosition;
var closeKeyPosition;
for (i = 0; i < len; i++) {
tokenTemplate = Twig.token.definitions[i];
firstKeyPosition = template.indexOf(tokenTemplate.open);
closeKeyPosition = template.indexOf(tokenTemplate.close);
Twig.log.trace('Twig.token.findStart: ', 'Searching for ', tokenTemplate.open, ' found at ', firstKeyPosition);
// Special handling for mismatched tokens
if (firstKeyPosition >= 0) {
// This token matches the template
if (tokenTemplate.open.length !== tokenTemplate.close.length) {
// This token has mismatched closing and opening tags
if (closeKeyPosition < 0) {
// This token's closing tag does not match the template
continue;
}
}
}
// Does this token occur before any other types?
if (firstKeyPosition >= 0 && (output.position === null || firstKeyPosition < output.position)) {
output.position = firstKeyPosition;
output.def = tokenTemplate;
closePosition = closeKeyPosition;
} else if (firstKeyPosition >= 0 && output.position !== null && firstKeyPosition === output.position) {
/* This token exactly matches another token,
greedily match to check if this token has a greater specificity */
if (tokenTemplate.open.length > output.def.open.length) {
// This token's opening tag is more specific than the previous match
output.position = firstKeyPosition;
output.def = tokenTemplate;
closePosition = closeKeyPosition;
} else if (tokenTemplate.open.length === output.def.open.length) {
if (tokenTemplate.close.length > output.def.close.length) {
// This token's opening tag is as specific as the previous match,
// but the closing tag has greater specificity
if (closeKeyPosition >= 0 && closeKeyPosition < closePosition) {
// This token's closing tag exists in the template,
// and it occurs sooner than the previous match
output.position = firstKeyPosition;
output.def = tokenTemplate;
closePosition = closeKeyPosition;
}
} else if (closeKeyPosition >= 0 && closeKeyPosition < closePosition) {
// This token's closing tag is not more specific than the previous match,
// but it occurs sooner than the previous match
output.position = firstKeyPosition;
output.def = tokenTemplate;
closePosition = closeKeyPosition;
}
}
}
}
return output;
};
Twig.token.findEnd = function (template, tokenDef, start) {
var end = null;
var found = false;
var offset = 0;
// String position variables
var strPos = null;
var strFound = null;
var pos = null;
var endOffset = null;
var thisStrPos = null;
var endStrPos = null;
// For loop variables
var i;
var l;
while (!found) {
strPos = null;
strFound = null;
pos = template.indexOf(tokenDef.close, offset);
if (pos >= 0) {
end = pos;
found = true;
} else {
// Throw an exception
throw new Twig.Error('Unable to find closing bracket \'' + tokenDef.close + '\' opened near template position ' + start);
}
// Ignore quotes within comments; just look for the next comment close sequence,
// regardless of what comes before it. https://github.com/justjohn/twig.js/issues/95
if (tokenDef.type === Twig.token.type.comment) {
break;
}
// Ignore quotes within raw tag
// Fixes #283
if (tokenDef.type === Twig.token.type.raw) {
break;
}
l = Twig.token.strings.length;
for (i = 0; i < l; i += 1) {
thisStrPos = template.indexOf(Twig.token.strings[i], offset);
if (thisStrPos > 0 && thisStrPos < pos && (strPos === null || thisStrPos < strPos)) {
strPos = thisStrPos;
strFound = Twig.token.strings[i];
}
}
// We found a string before the end of the token, now find the string's end and set the search offset to it
if (strPos !== null) {
endOffset = strPos + 1;
end = null;
found = false;
for (;;) {
endStrPos = template.indexOf(strFound, endOffset);
if (endStrPos < 0) {
throw Twig.Error('Unclosed string in template');
}
// Ignore escaped quotes
if (template.slice(endStrPos - 1, endStrPos) === '\\') {
endOffset = endStrPos + 1;
} else {
offset = endStrPos + 1;
break;
}
}
}
}
return end;
};
/**
* Convert a template into high-level tokens.
*/
Twig.tokenize = function (template) {
var tokens = [];
// An offset for reporting errors locations and the position of the nodes in the template.
var currentPosition = 0;
// The start and type of the first token found in the template.
var foundToken = null;
// The end position of the matched token.
var end = null;
while (template.length > 0) {
// Find the first occurance of any token type in the template
foundToken = Twig.token.findStart(template);
Twig.log.trace('Twig.tokenize: ', 'Found token: ', foundToken);
if (foundToken.position === null) {
// No more tokens -> add the rest of the template as a raw-type token
tokens.push({
type: Twig.token.type.raw,
value: template,
position: {
start: currentPosition,
end: currentPosition + foundToken.position
}
});
template = '';
} else {
// Add a raw type token for anything before the start of the token
if (foundToken.position > 0) {
tokens.push({
type: Twig.token.type.raw,
value: template.slice(0, Math.max(0, foundToken.position)),
position: {
start: currentPosition,
end: currentPosition + Math.max(0, foundToken.position)
}
});
}
template = template.slice(foundToken.position + foundToken.def.open.length);
currentPosition += foundToken.position + foundToken.def.open.length;
// Find the end of the token
end = Twig.token.findEnd(template, foundToken.def, currentPosition);
Twig.log.trace('Twig.tokenize: ', 'Token ends at ', end);
tokens.push({
type: foundToken.def.type,
value: template.slice(0, Math.max(0, end)).trim(),
position: {
start: currentPosition - foundToken.def.open.length,
end: currentPosition + end + foundToken.def.close.length
}
});
if (template.slice(end + foundToken.def.close.length, end + foundToken.def.close.length + 1) === '\n') {
switch (foundToken.def.type) {
case 'logic_whitespace_pre':
case 'logic_whitespace_post':
case 'logic_whitespace_both':
case 'logic':
// Newlines directly after logic tokens are ignored
end += 1;
break;
default:
break;
}
}
template = template.slice(end + foundToken.def.close.length);
// Increment the position in the template
currentPosition += end + foundToken.def.close.length;
}
}
return tokens;
};
Twig.compile = function (tokens) {
var self = this;
try {
// Output and intermediate stacks
var output = [];
var stack = [];
// The tokens between open and close tags
var intermediateOutput = [];
var token = null;
var logicToken = null;
var unclosedToken = null;
// Temporary previous token.
var prevToken = null;
// Temporary previous output.
var prevOutput = null;
// Temporary previous intermediate output.
var prevIntermediateOutput = null;
// The previous token's template
var prevTemplate = null;
// Token lookahead
var nextToken = null;
// The output token
var tokOutput = null;
// Logic Token values
var type = null;
var open = null;
var next = null;
var compileOutput = function compileOutput(token) {
Twig.expression.compile.call(self, token);
if (stack.length > 0) {
intermediateOutput.push(token);
} else {
output.push(token);
}
};
var compileLogic = function compileLogic(token) {
// Compile the logic token
logicToken = Twig.logic.compile.call(self, token);
logicToken.position = token.position;
type = logicToken.type;
open = Twig.logic.handler[type].open;
next = Twig.logic.handler[type].next;
Twig.log.trace('Twig.compile: ', 'Compiled logic token to ', logicToken, ' next is: ', next, ' open is : ', open);
// Not a standalone token, check logic stack to see if this is expected
if (open !== undefined && !open) {
prevToken = stack.pop();
prevTemplate = Twig.logic.handler[prevToken.type];
if (!prevTemplate.next.includes(type)) {
throw new Error(type + ' not expected after a ' + prevToken.type);
}
prevToken.output = prevToken.output || [];
prevToken.output = prevToken.output.concat(intermediateOutput);
intermediateOutput = [];
tokOutput = {
type: Twig.token.type.logic,
token: prevToken,
position: {
open: prevToken.position,
close: token.position
}
};
if (stack.length > 0) {
intermediateOutput.push(tokOutput);
} else {
output.push(tokOutput);
}
}
// This token requires additional tokens to complete the logic structure.
if (next !== undefined && next.length > 0) {
Twig.log.trace('Twig.compile: ', 'Pushing ', logicToken, ' to logic stack.');
if (stack.length > 0) {
// Put any currently held output into the output list of the logic operator
// currently at the head of the stack before we push a new one on.
prevToken = stack.pop();
prevToken.output = prevToken.output || [];
prevToken.output = prevToken.output.concat(intermediateOutput);
stack.push(prevToken);
intermediateOutput = [];
}
// Push the new logic token onto the logic stack
stack.push(logicToken);
} else if (open !== undefined && open) {
tokOutput = {
type: Twig.token.type.logic,
token: logicToken,
position: logicToken.position
};
// Standalone token (like {% set ... %}
if (stack.length > 0) {
intermediateOutput.push(tokOutput);
} else {
output.push(tokOutput);
}
}
};
while (tokens.length > 0) {
token = tokens.shift();
prevOutput = output[output.length - 1];
prevIntermediateOutput = intermediateOutput[intermediateOutput.length - 1];
nextToken = tokens[0];
Twig.log.trace('Compiling token ', token);
switch (token.type) {
case Twig.token.type.raw:
if (stack.length > 0) {
intermediateOutput.push(token);
} else {
output.push(token);
}
break;
case Twig.token.type.logic:
compileLogic.call(self, token);
break;
// Do nothing, comments should be ignored
case Twig.token.type.comment:
break;
case Twig.token.type.output:
compileOutput.call(self, token);
break;
// Kill whitespace ahead and behind this token
case Twig.token.type.logicWhitespacePre:
case Twig.token.type.logicWhitespacePost:
case Twig.token.type.logicWhitespaceBoth:
case Twig.token.type.outputWhitespacePre:
case Twig.token.type.outputWhitespacePost:
case Twig.token.type.outputWhitespaceBoth:
if (token.type !== Twig.token.type.outputWhitespacePost && token.type !== Twig.token.type.logicWhitespacePost) {
if (prevOutput) {
// If the previous output is raw, pop it off
if (prevOutput.type === Twig.token.type.raw) {
output.pop();
prevOutput.value = prevOutput.value.trimEnd();
// Repush the previous output
output.push(prevOutput);
}
}
if (prevIntermediateOutput) {
// If the previous intermediate output is raw, pop it off
if (prevIntermediateOutput.type === Twig.token.type.raw) {
intermediateOutput.pop();
prevIntermediateOutput.value = prevIntermediateOutput.value.trimEnd();
// Repush the previous intermediate output
intermediateOutput.push(prevIntermediateOutput);
}
}
}
// Compile this token
switch (token.type) {
case Twig.token.type.outputWhitespacePre:
case Twig.token.type.outputWhitespacePost:
case Twig.token.type.outputWhitespaceBoth:
compileOutput.call(self, token);
break;
case Twig.token.type.logicWhitespacePre:
case Twig.token.type.logicWhitespacePost:
case Twig.token.type.logicWhitespaceBoth:
compileLogic.call(self, token);
break;
default:
break;
}
if (token.type !== Twig.token.type.outputWhitespacePre && token.type !== Twig.token.type.logicWhitespacePre) {
if (nextToken) {
// If the next token is raw, shift it out
if (nextToken.type === Twig.token.type.raw) {
tokens.shift();
nextToken.value = nextToken.value.trimStart();
// Unshift the next token
tokens.unshift(nextToken);
}
}
}
break;
default:
break;
}
Twig.log.trace('Twig.compile: ', ' Output: ', output, ' Logic Stack: ', stack, ' Pending Output: ', intermediateOutput);
}
// Verify that there are no logic tokens left in the stack.
if (stack.length > 0) {
unclosedToken = stack.pop();
throw new Error('Unable to find an end tag for ' + unclosedToken.type + ', expecting one of ' + unclosedToken.next);
}
return output;
} catch (error) {
if (self.options.rethrow) {
if (error.type === 'TwigException' && !error.file) {
error.file = self.id;
}
throw error;
} else {
Twig.log.error('Error compiling twig template ' + self.id + ': ');
if (error.stack) {
Twig.log.error(error.stack);
} else {
Twig.log.error(error.toString());
}
}
}
};
function handleException(state, ex) {
if (state.template.options.rethrow) {
if (typeof ex === 'string') {
ex = new Twig.Error(ex);
}
if (ex.type === 'TwigException' && !ex.file) {
ex.file = state.template.id;
}
throw ex;
} else {
Twig.log.error('Error parsing twig template ' + state.template.id + ': ');
if (ex.stack) {
Twig.log.error(ex.stack);
} else {
Twig.log.error(ex.toString());
}
if (Twig.debug) {
return ex.toString();
}
}
}
/**
* Tokenize and compile a string template.
*
* @param {string} data The template.
*
* @return {Array} The compiled tokens.
*/
Twig.prepare = function (data) {
// Tokenize
Twig.log.debug('Twig.prepare: ', 'Tokenizing ', data);
var rawTokens = Twig.tokenize.call(this, data);
// Compile
Twig.log.debug('Twig.prepare: ', 'Compiling ', rawTokens);
var tokens = Twig.compile.call(this, rawTokens);
Twig.log.debug('Twig.prepare: ', 'Compiled ', tokens);
return tokens;
};
/**
* 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) {
var autoescape = this.options.autoescape;
if (!autoescape) {
return output.join('');
}
var strategy = typeof autoescape === 'string' ? autoescape : 'html';
var escapedOutput = output.map(function (str) {
if (str && str.twigMarkup !== true && str.twigMarkup !== strategy && !(strategy === 'html' && str.twigMarkup === 'html_attr')) {
str = Twig.filters.escape(str, [strategy]);
}
return str;
});
if (escapedOutput.length === 0) {
return '';
}
var joinedOutput = escapedOutput.join('');
if (joinedOutput.length === 0) {
return '';
}
return new Twig.Markup(joinedOutput, true);
};
// Namespace for template storage and retrieval
Twig.Templates = {
/**
* Registered template loaders - use Twig.Templates.registerLoader to add supported loaders
* @type {Object}
*/
loaders: {},
/**
* Registered template parsers - use Twig.Templates.registerParser to add supported parsers
* @type {Object}
*/
parsers: {},
/**
* Cached / loaded templates
* @type {Object}
*/
registry: {}
};
/**
* Is this id valid for a twig template?
*
* @param {string} id The ID to check.
*
* @throws {Twig.Error} If the ID is invalid or used.
* @return {boolean} True if the ID is valid.
*/
Twig.validateId = function (id) {
if (id === 'prototype') {
throw new Twig.Error(id + ' is not a valid twig identifier');
} else if (Twig.cache && Object.hasOwnProperty.call(Twig.Templates.registry, id)) {
throw new Twig.Error('There is already a template with the ID ' + id);
}
return true;
};
/**
* Register a template loader
*
* @example
* Twig.extend(function (Twig) {
* Twig.Templates.registerLoader('custom_loader', function (location, params, callback, errorCallback) {
* // ... load the template ...
* params.data = loadedTemplateData;
* // create and return the template
* var template = new Twig.Template(params);
* if (typeof callback === 'function') {
* callback(template);
* }
* return template;
* });
* });
*
* @param {String} methodName The method this loader is intended for (ajax, fs)
* @param {Function} func The function to execute when loading the template
* @param {Object|undefined} scope Optional scope parameter to bind func to
*
* @throws Twig.Error
*
* @return {void}
*/
Twig.Templates.registerLoader = function (methodName, func, scope) {
if (typeof func !== 'function') {
throw new Twig.Error('Unable to add loader for ' + methodName + ': Invalid function reference given.');
}
if (scope) {
func = func.bind(scope);
}
this.loaders[methodName] = func;
};
/**
* Remove a registered loader
*
* @param {String} methodName The method name for the loader you wish to remove
*
* @return {void}
*/
Twig.Templates.unRegisterLoader = function (methodName) {
if (this.isRegisteredLoader(methodName)) {
delete this.loaders[methodName];
}
};
/**
* See if a loader is registered by its method name
*
* @param {String} methodName The name of the loader you are looking for
*
* @return {boolean}
*/
Twig.Templates.isRegisteredLoader = function (methodName) {
return Object.hasOwnProperty.call(this.loaders, methodName);
};
/**
* 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} methodName 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 (methodName, func, scope) {
if (typeof func !== 'function') {
throw new Twig.Error('Unable to add parser for ' + methodName + ': Invalid function regerence given.');
}
if (scope) {
func = func.bind(scope);
}
this.parsers[methodName] = func;
};
/**
* Remove a registered parser
*
* @param {String} methodName The method name for the parser you wish to remove
*
* @return {void}
*/
Twig.Templates.unRegisterParser = function (methodName) {
if (this.isRegisteredParser(methodName)) {
delete this.parsers[methodName];
}
};
/**
* See if a parser is registered by its method name
*
* @param {String} methodName The name of the parser you are looking for
*
* @return {boolean}
*/
Twig.Templates.isRegisteredParser = function (methodName) {
return Object.hasOwnProperty.call(this.parsers, methodName);
};
/**
* 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 (!Object.hasOwnProperty.call(Twig.Templates.registry, id)) {
return null;
}
return Twig.Templates.registry[id];
};
/**
* Load a template from a remote location using AJAX and saves in with the given ID.
*
* Available parameters:
*
* async: Should the HTTP request be performed asynchronously.
* Defaults to true.
* method: What method should be used to load the template
* (fs or ajax)
* parser: What method should be used to parse the template
* (twig or source)
* precompiled: Has the template already been compiled.
*
* @param {string} location The remote URL to load as a template.
* @param {Object} params The template parameters.
* @param {function} callback A callback triggered when the template finishes loading.
* @param {function} errorCallback A callback triggered if an error occurs loading the template.
*
*
*/
Twig.Templates.loadRemote = function (location, params, callback, errorCallback) {
// Default to the URL so the template is cached.
var id = typeof params.id === 'undefined' ? location : params.id;
var cached = Twig.Templates.registry[id];
// Check for existing template
if (Twig.cache && typeof cached !== 'undefined') {
// A template is already saved with the given id.
if (typeof callback === 'function') {
callback(cached);
}
// TODO: if async, return deferred promise
return cached;
}
// If the parser name hasn't been set, default it to twig
params.parser = params.parser || 'twig';
params.id = id;
// Default to async
if (typeof params.async === 'undefined') {
params.async = true;
}
// Assume 'fs' if the loader is not defined
var loader = this.loaders[params.method] || this.loaders.fs;
return loader.call(this, location, params, callback, errorCallback);
};
// Determine object type
function is(type, obj) {
var clas = Object.prototype.toString.call(obj).slice(8, -1);
return obj !== undefined && obj !== null && clas === type;
}
/**
* A wrapper for template blocks.
*
* @param {Twig.Template} The template that the block was originally defined in.
* @param {Object} The compiled block token.
*/
Twig.Block = function (template, token) {
this.template = template;
this.token = token;
};
/**
* Render the block using a specific parse state and context.
*
* @param {Twig.ParseState} parseState
* @param {Object} context
*
* @return {Promise}
*/
Twig.Block.prototype.render = function (parseState, context) {
var originalTemplate = parseState.template;
var promise;
parseState.template = this.template;
if (this.token.expression) {
promise = Twig.expression.parseAsync.call(parseState, this.token.output, context);
} else {
promise = parseState.parseAsync(this.token.output, context);
}
return promise.then(function (value) {
return Twig.expression.parseAsync.call(parseState, {
type: Twig.expression.type.string,
value: value
}, context);
}).then(function (output) {
parseState.template = originalTemplate;
return output;
});
};
/**
* Holds the state needed to parse a template.
*
* @param {Twig.Template} template The template that the tokens being parsed are associated with.
* @param {Object} blockOverrides Any blocks that should override those defined in the associated template.
*/
Twig.ParseState = function (template, blockOverrides, context) {
this.renderedBlocks = {};
this.overrideBlocks = blockOverrides === undefined ? {} : blockOverrides;
this.context = context === undefined ? {} : context;
this.macros = {};
this.nestingStack = [];
this.template = template;
};
/**
* Get a block by its name, resolving in the following order:
* - override blocks specified when initialized (except when excluded)
* - blocks resolved from the associated template
* - blocks resolved from the parent template when extending
*
* @param {String} name The name of the block to return.
* @param {Boolean} checkOnlyInheritedBlocks Whether to skip checking the overrides and associated template, will not skip by default.
*
* @return {Twig.Block|undefined}
*/
Twig.ParseState.prototype.getBlock = function (name, checkOnlyInheritedBlocks) {
var block;
if (checkOnlyInheritedBlocks !== true) {
// Blocks specified when initialized
block = this.overrideBlocks[name];
}
if (block === undefined) {
// Block defined by the associated template
block = this.template.getBlock(name, checkOnlyInheritedBlocks);
}
if (block === undefined && this.template.parentTemplate !== null) {
// Block defined in the parent template when extending
block = this.template.parentTemplate.getBlock(name);
}
return block;
};
/**
* Get all the available blocks, resolving in the following order:
* - override blocks specified when initialized
* - blocks resolved from the associated template
* - blocks resolved from the parent template when extending (except when excluded)
*
* @param {Boolean} includeParentBlocks Whether to get blocks from the parent template when extending, will always do so by default.
*
* @return {Object}
*/
Twig.ParseState.prototype.getBlocks = function (includeParentBlocks) {
var blocks = {};
if (includeParentBlocks !== false && this.template.parentTemplate !== null &&
// Prevent infinite loop
this.template.parentTemplate !== this.template) {
// Blocks from the parent template when extending
blocks = this.template.parentTemplate.getBlocks();
}
blocks = _objectSpread(_objectSpread(_objectSpread({}, blocks), this.template.getBlocks()), this.overrideBlocks);
return blocks;
};
/**
* Get the closest token of a specific type to the current nest level.
*
* @param {String} type The logic token type
*
* @return {Object}
*/
Twig.ParseState.prototype.getNestingStackToken = function (type) {
var matchingToken;
this.nestingStack.forEach(function (token) {
if (matchingToken === undefined && token.type === type) {
matchingToken = token;
}
});
return matchingToken;
};
/**
* Parse a set of tokens using the current state.
*
* @param {Array} tokens The compiled tokens.
* @param {Object} context The context to set the state to while parsing.
* @param {Boolean} allowAsync Whether to parse asynchronously.
* @param {Object} blocks Blocks that should override any defined while parsing.
*
* @return {String} The rendered tokens.
*
*/
Twig.ParseState.prototype.parse = function (tokens, context, allowAsync) {
var state = this;
var output = [];
// Store any error that might be thrown by the promise chain.
var err = null;
// This will be set to isAsync if template renders synchronously
var isAsync = true;
var promise = null;
// Track logic chains
var chain = true;
if (context) {
state.context = context;
}
/*
* Extracted into it's own function such that the function
* does not get recreated over and over again in the `forEach`
* loop below. This method can be compiled and optimized
* a single time instead of being recreated on each iteration.
*/
function outputPush(o) {
output.push(o);
}
function parseTokenLogic(logic) {
if (typeof logic.chain !== 'undefined') {
chain = logic.chain;
}
if (typeof logic.context !== 'undefined') {
state.context = logic.context;
}
if (typeof logic.output !== 'undefined') {
output.push(logic.output);
}
}
promise = Twig.async.forEach(tokens, function (token) {
Twig.log.debug('Twig.ParseState.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:
return Twig.logic.parseAsync.call(state, token.token /* logicToken */, state.context, chain).then(parseTokenLogic);
case Twig.token.type.comment:
// Do nothing, comments should be ignored
break;
// Fall through whitespace to output
case Twig.token.type.outputWhitespacePre:
case Twig.token.type.outputWhitespacePost:
case Twig.token.type.outputWhitespaceBoth:
case Twig.token.type.output:
Twig.log.debug('Twig.ParseState.parse: ', 'Output token: ', token.stack);
// Parse the given expression in the given context
return Twig.expression.parseAsync.call(state, token.stack, state.context).then(outputPush);
default:
break;
}
}).then(function () {
output = Twig.output.call(state.template, output);
isAsync = false;
return output;
})["catch"](function (error) {
if (allowAsync) {
handleException(state, error);
}
err = error;
});
// If `allowAsync` we will always return a promise since we do not
// know in advance if we are going to run asynchronously or not.
if (allowAsync) {
return promise;
}
// Handle errors here if we fail synchronously.
if (err !== null) {
return handleException(state, err);
}
// If `allowAsync` is not true we should not allow the user
// to use asynchronous functions or filters.
if (isAsync) {
throw new Twig.Error('You are using Twig.js in sync mode in combination with async extensions.');
}
return output;
};
/**
* Create a new twig.js template.
*
* Parameters: {
* data: The template, either pre-compiled tokens or a string template
* id: The name of this template
* }
*
* @param {Object} params The template parameters.
*/
Twig.Template = function (params) {
var data = params.data,
id = params.id,
base = params.base,
path = params.path,
url = params.url,
name = params.name,
method = params.method,
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.
// base: The base template (if any)
// options: {
// Compiler/parser