twig
Version:
JS port of the Twig templating language.
1,408 lines (1,219 loc) • 241 kB
JavaScript
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory((function webpackLoadOptionalExternalModule() { try { return require("fs"); } catch(e) {} }()), require("path"));
else if(typeof define === 'function' && define.amd)
define(["fs", "path"], factory);
else if(typeof exports === 'object')
exports["Twig"] = factory((function webpackLoadOptionalExternalModule() { try { return require("fs"); } catch(e) {} }()), require("path"));
else
root["Twig"] = factory(root["fs"], root["path"]);
})(this, function(__WEBPACK_EXTERNAL_MODULE_18__, __WEBPACK_EXTERNAL_MODULE_19__) {
return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId])
/******/ return installedModules[moduleId].exports;
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ exports: {},
/******/ id: moduleId,
/******/ loaded: false
/******/ };
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/ // Load entry module and return exports
/******/ return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {
/**
* Twig.js 0.8.9
*
* @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.8.9'
};
__webpack_require__(1)(Twig);
__webpack_require__(2)(Twig);
__webpack_require__(3)(Twig);
__webpack_require__(5)(Twig);
__webpack_require__(6)(Twig);
__webpack_require__(7)(Twig);
__webpack_require__(16)(Twig);
__webpack_require__(17)(Twig);
__webpack_require__(20)(Twig);
__webpack_require__(21)(Twig);
__webpack_require__(22)(Twig);
__webpack_require__(23)(Twig);
__webpack_require__(24)(Twig);
__webpack_require__(25)(Twig);
module.exports = Twig.exports;
/***/ },
/* 1 */
/***/ function(module, exports) {
// ## 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.placeholders = {
parent: "{{|PARENT|}}"
};
/**
* Fallback for Array.indexOf for IE8 et al
*/
Twig.indexOf = function (arr, searchElement /*, fromIndex */ ) {
if (Array.prototype.hasOwnProperty("indexOf")) {
return arr.indexOf(searchElement);
}
if (arr === void 0 || arr === null) {
throw new TypeError();
}
var t = Object(arr);
var len = t.length >>> 0;
if (len === 0) {
return -1;
}
var n = 0;
if (arguments.length > 0) {
n = Number(arguments[1]);
if (n !== n) { // shortcut for verifying if it's NaN
n = 0;
} else if (n !== 0 && n !== Infinity && n !== -Infinity) {
n = (n > 0 || -1) * Math.floor(Math.abs(n));
}
}
if (n >= len) {
// console.log("indexOf not found1 ", JSON.stringify(searchElement), JSON.stringify(arr));
return -1;
}
var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0);
for (; k < len; k++) {
if (k in t && t[k] === searchElement) {
return k;
}
}
if (arr == searchElement) {
return 0;
}
// console.log("indexOf not found2 ", JSON.stringify(searchElement), JSON.stringify(arr));
return -1;
}
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'
};
/**
* 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.output_whitespace_pre,
open: '{{-',
close: '}}'
},
{
type: Twig.token.type.output_whitespace_post,
open: '{{',
close: '-}}'
},
{
type: Twig.token.type.output_whitespace_both,
open: '{{-',
close: '-}}'
},
{
type: Twig.token.type.logic_whitespace_pre,
open: '{%-',
close: '%}'
},
{
type: Twig.token.type.logic_whitespace_post,
open: '{%',
close: '-%}'
},
{
type: Twig.token.type.logic_whitespace_both,
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,
close_position: null,
def: null
},
i,
token_template,
first_key_position,
close_key_position;
for (i=0;i<Twig.token.definitions.length;i++) {
token_template = Twig.token.definitions[i];
first_key_position = template.indexOf(token_template.open);
close_key_position = template.indexOf(token_template.close);
Twig.log.trace("Twig.token.findStart: ", "Searching for ", token_template.open, " found at ", first_key_position);
//Special handling for mismatched tokens
if (first_key_position >= 0) {
//This token matches the template
if (token_template.open.length !== token_template.close.length) {
//This token has mismatched closing and opening tags
if (close_key_position < 0) {
//This token's closing tag does not match the template
continue;
}
}
}
// Does this token occur before any other types?
if (first_key_position >= 0 && (output.position === null || first_key_position < output.position)) {
output.position = first_key_position;
output.def = token_template;
output.close_position = close_key_position;
} else if (first_key_position >= 0 && output.position !== null && first_key_position === output.position) {
/*This token exactly matches another token,
greedily match to check if this token has a greater specificity*/
if (token_template.open.length > output.def.open.length) {
//This token's opening tag is more specific than the previous match
output.position = first_key_position;
output.def = token_template;
output.close_position = close_key_position;
} else if (token_template.open.length === output.def.open.length) {
if (token_template.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 (close_key_position >= 0 && close_key_position < output.close_position) {
//This token's closing tag exists in the template,
//and it occurs sooner than the previous match
output.position = first_key_position;
output.def = token_template;
output.close_position = close_key_position;
}
} else if (close_key_position >= 0 && close_key_position < output.close_position) {
//This token's closing tag is not more specific than the previous match,
//but it occurs sooner than the previous match
output.position = first_key_position;
output.def = token_template;
output.close_position = close_key_position;
}
}
}
}
delete output['close_position'];
return output;
};
Twig.token.findEnd = function (template, token_def, start) {
var end = null,
found = false,
offset = 0,
// String position variables
str_pos = null,
str_found = null,
pos = null,
end_offset = null,
this_str_pos = null,
end_str_pos = null,
// For loop variables
i,
l;
while (!found) {
str_pos = null;
str_found = null;
pos = template.indexOf(token_def.close, offset);
if (pos >= 0) {
end = pos;
found = true;
} else {
// throw an exception
throw new Twig.Error("Unable to find closing bracket '" + token_def.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 (token_def.type === Twig.token.type.comment) {
break;
}
// Ignore quotes within raw tag
// Fixes #283
if (token_def.type === Twig.token.type.raw) {
break;
}
l = Twig.token.strings.length;
for (i = 0; i < l; i += 1) {
this_str_pos = template.indexOf(Twig.token.strings[i], offset);
if (this_str_pos > 0 && this_str_pos < pos &&
(str_pos === null || this_str_pos < str_pos)) {
str_pos = this_str_pos;
str_found = 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 (str_pos !== null) {
end_offset = str_pos + 1;
end = null;
found = false;
while (true) {
end_str_pos = template.indexOf(str_found, end_offset);
if (end_str_pos < 0) {
throw "Unclosed string in template";
}
// Ignore escaped quotes
if (template.substr(end_str_pos - 1, 1) !== "\\") {
offset = end_str_pos + 1;
break;
} else {
end_offset = end_str_pos + 1;
}
}
}
}
return end;
};
/**
* Convert a template into high-level tokens.
*/
Twig.tokenize = function (template) {
var tokens = [],
// An offset for reporting errors locations in the template.
error_offset = 0,
// The start and type of the first token found in the template.
found_token = null,
// The end position of the matched token.
end = null;
while (template.length > 0) {
// Find the first occurance of any token type in the template
found_token = Twig.token.findStart(template);
Twig.log.trace("Twig.tokenize: ", "Found token: ", found_token);
if (found_token.position !== null) {
// Add a raw type token for anything before the start of the token
if (found_token.position > 0) {
tokens.push({
type: Twig.token.type.raw,
value: template.substring(0, found_token.position)
});
}
template = template.substr(found_token.position + found_token.def.open.length);
error_offset += found_token.position + found_token.def.open.length;
// Find the end of the token
end = Twig.token.findEnd(template, found_token.def, error_offset);
Twig.log.trace("Twig.tokenize: ", "Token ends at ", end);
tokens.push({
type: found_token.def.type,
value: template.substring(0, end).trim()
});
if (template.substr( end + found_token.def.close.length, 1 ) === "\n") {
switch (found_token.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;
}
}
template = template.substr(end + found_token.def.close.length);
// Increment the position in the template
error_offset += end + found_token.def.close.length;
} else {
// No more tokens -> add the rest of the template as a raw-type token
tokens.push({
type: Twig.token.type.raw,
value: template
});
template = '';
}
}
return tokens;
};
Twig.compile = function (tokens) {
try {
// Output and intermediate stacks
var output = [],
stack = [],
// The tokens between open and close tags
intermediate_output = [],
token = null,
logic_token = null,
unclosed_token = null,
// Temporary previous token.
prev_token = null,
// Temporary previous output.
prev_output = null,
// Temporary previous intermediate output.
prev_intermediate_output = null,
// The previous token's template
prev_template = null,
// Token lookahead
next_token = null,
// The output token
tok_output = null,
// Logic Token values
type = null,
open = null,
next = null;
var compile_output = function(token) {
Twig.expression.compile.apply(this, [token]);
if (stack.length > 0) {
intermediate_output.push(token);
} else {
output.push(token);
}
};
var compile_logic = function(token) {
// Compile the logic token
logic_token = Twig.logic.compile.apply(this, [token]);
type = logic_token.type;
open = Twig.logic.handler[type].open;
next = Twig.logic.handler[type].next;
Twig.log.trace("Twig.compile: ", "Compiled logic token to ", logic_token,
" next is: ", next, " open is : ", open);
// Not a standalone token, check logic stack to see if this is expected
if (open !== undefined && !open) {
prev_token = stack.pop();
prev_template = Twig.logic.handler[prev_token.type];
if (Twig.indexOf(prev_template.next, type) < 0) {
throw new Error(type + " not expected after a " + prev_token.type);
}
prev_token.output = prev_token.output || [];
prev_token.output = prev_token.output.concat(intermediate_output);
intermediate_output = [];
tok_output = {
type: Twig.token.type.logic,
token: prev_token
};
if (stack.length > 0) {
intermediate_output.push(tok_output);
} else {
output.push(tok_output);
}
}
// This token requires additional tokens to complete the logic structure.
if (next !== undefined && next.length > 0) {
Twig.log.trace("Twig.compile: ", "Pushing ", logic_token, " 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.
prev_token = stack.pop();
prev_token.output = prev_token.output || [];
prev_token.output = prev_token.output.concat(intermediate_output);
stack.push(prev_token);
intermediate_output = [];
}
// Push the new logic token onto the logic stack
stack.push(logic_token);
} else if (open !== undefined && open) {
tok_output = {
type: Twig.token.type.logic,
token: logic_token
};
// Standalone token (like {% set ... %}
if (stack.length > 0) {
intermediate_output.push(tok_output);
} else {
output.push(tok_output);
}
}
};
while (tokens.length > 0) {
token = tokens.shift();
prev_output = output[output.length - 1];
prev_intermediate_output = intermediate_output[intermediate_output.length - 1];
next_token = tokens[0];
Twig.log.trace("Compiling token ", token);
switch (token.type) {
case Twig.token.type.raw:
if (stack.length > 0) {
intermediate_output.push(token);
} else {
output.push(token);
}
break;
case Twig.token.type.logic:
compile_logic.call(this, token);
break;
// Do nothing, comments should be ignored
case Twig.token.type.comment:
break;
case Twig.token.type.output:
compile_output.call(this, token);
break;
//Kill whitespace ahead and behind this token
case Twig.token.type.logic_whitespace_pre:
case Twig.token.type.logic_whitespace_post:
case Twig.token.type.logic_whitespace_both:
case Twig.token.type.output_whitespace_pre:
case Twig.token.type.output_whitespace_post:
case Twig.token.type.output_whitespace_both:
if (token.type !== Twig.token.type.output_whitespace_post && token.type !== Twig.token.type.logic_whitespace_post) {
if (prev_output) {
//If the previous output is raw, pop it off
if (prev_output.type === Twig.token.type.raw) {
output.pop();
//If the previous output is not just whitespace, trim it
if (prev_output.value.match(/^\s*$/) === null) {
prev_output.value = prev_output.value.trim();
//Repush the previous output
output.push(prev_output);
}
}
}
if (prev_intermediate_output) {
//If the previous intermediate output is raw, pop it off
if (prev_intermediate_output.type === Twig.token.type.raw) {
intermediate_output.pop();
//If the previous output is not just whitespace, trim it
if (prev_intermediate_output.value.match(/^\s*$/) === null) {
prev_intermediate_output.value = prev_intermediate_output.value.trim();
//Repush the previous intermediate output
intermediate_output.push(prev_intermediate_output);
}
}
}
}
//Compile this token
switch (token.type) {
case Twig.token.type.output_whitespace_pre:
case Twig.token.type.output_whitespace_post:
case Twig.token.type.output_whitespace_both:
compile_output.call(this, token);
break;
case Twig.token.type.logic_whitespace_pre:
case Twig.token.type.logic_whitespace_post:
case Twig.token.type.logic_whitespace_both:
compile_logic.call(this, token);
break;
}
if (token.type !== Twig.token.type.output_whitespace_pre && token.type !== Twig.token.type.logic_whitespace_pre) {
if (next_token) {
//If the next token is raw, shift it out
if (next_token.type === Twig.token.type.raw) {
tokens.shift();
//If the next token is not just whitespace, trim it
if (next_token.value.match(/^\s*$/) === null) {
next_token.value = next_token.value.trim();
//Unshift the next token
tokens.unshift(next_token);
}
}
}
}
break;
}
Twig.log.trace("Twig.compile: ", " Output: ", output,
" Logic Stack: ", stack,
" Pending Output: ", intermediate_output );
}
// Verify that there are no logic tokens left in the stack.
if (stack.length > 0) {
unclosed_token = stack.pop();
throw new Error("Unable to find an end tag for " + unclosed_token.type +
", expecting one of " + unclosed_token.next);
}
return output;
} catch (ex) {
Twig.log.error("Error compiling twig template " + this.id + ": ");
if (ex.stack) {
Twig.log.error(ex.stack);
} else {
Twig.log.error(ex.toString());
}
if (this.options.rethrow) throw ex;
}
};
/**
* 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.apply(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.apply(that, [token.stack, context]));
break;
}
});
return Twig.output.apply(this, [output]);
} catch (ex) {
Twig.log.error("Error parsing twig template " + this.id + ": ");
if (ex.stack) {
Twig.log.error(ex.stack);
} else {
Twig.log.error(ex.toString());
}
if (this.options.rethrow) throw ex;
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) {
var tokens, raw_tokens;
// Tokenize
Twig.log.debug("Twig.prepare: ", "Tokenizing ", data);
raw_tokens = Twig.tokenize.apply(this, [data]);
// Compile
Twig.log.debug("Twig.prepare: ", "Compiling ", raw_tokens);
tokens = Twig.compile.apply(this, [raw_tokens]);
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) {
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 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 && Twig.Templates.registry.hasOwnProperty(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, error_callback) {
* // ... 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} method_name 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(method_name, func, scope) {
if (typeof func !== 'function') {
throw new Twig.Error('Unable to add loader for ' + method_name + ': Invalid function reference given.');
}
if (scope) {
func = func.bind(scope);
}
this.loaders[method_name] = func;
};
/**
* Remove a registered loader
*
* @param {String} method_name The method name for the loader you wish to remove
*
* @return {void}
*/
Twig.Templates.unRegisterLoader = function(method_name) {
if (this.isRegisteredLoader(method_name)) {
delete this.loaders[method_name];
}
};
/**
* See if a loader is registered by its method name
*
* @param {String} method_name The name of the loader you are looking for
*
* @return {boolean}
*/
Twig.Templates.isRegisteredLoader = function(method_name) {
return this.loaders.hasOwnProperty(method_name);
};
/**
* 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];
};
/**
* 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} error_callback A callback triggered if an error occurs loading the template.
*
*
*/
Twig.Templates.loadRemote = function(location, params, callback, error_callback) {
var loader;
// Default to async
if (params.async === undefined) {
params.async = true;
}
// Default to the URL so the template is cached.
if (params.id === undefined) {
params.id = location;
}
// Check for existing template
if (Twig.cache && Twig.Templates.registry.hasOwnProperty(params.id)) {
// A template is already saved with the given id.
if (typeof callback === 'function') {
callback(Twig.Templates.registry[params.id]);
}
// TODO: if async, return deferred promise
return Twig.Templates.registry[params.id];
}
//if the parser name hasn't been set, default it to twig
params.parser = params.parser || 'twig';
// Assume 'fs' if the loader is not defined
loader = this.loaders[params.method] || this.loaders.fs;
return loader.apply(this, arguments);
};
// Determine object type
function is(type, obj) {
var clas = Object.prototype.toString.call(obj).slice(8, -1);
return obj !== undefined && obj !== null && clas === type;
}
/**
* 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,
macros = params.macros || {},
base = params.base,
path = params.path,
url = params.url,
name = params.name,
method = params.method,
// 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.method = method;
this.base = base;
this.path = path;
this.url = url;
this.name = name;
this.macros = macros;
this.options = options;
this.reset(blocks);
if (is('String', data)) {
this.tokens = Twig.prepare.apply(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 || {};
var output,
url;
this.context = context || {};
// Clear any previous state
this.reset();
if (params.blocks) {
this.blocks = params.blocks;
}
if (params.macros) {
this.macros = params.macros;
}
output = Twig.parse.apply(this, [this.tokens, this.context]);
// Does this template extend another
if (this.extend) {
var ext_template;
// check if the template is provided inline
if ( this.options.allowInlineIncludes ) {
ext_template = Twig.Templates.load(this.extend);
if ( ext_template ) {
ext_template.options = this.options;
}
}
// check for the template file via include
if (!ext_template) {
url = Twig.path.parsePath(this, this.extend);
ext_template = Twig.Templates.loadRemote(url, {
method: this.getLoaderMethod(),
base: this.base,
async: false,
id: url,
options: this.options
});
}
this.parent = ext_template;
return this.parent.render(this.context, {
blocks: this.blocks
});
}
if (params.output == 'blocks') {
return this.blocks;
} else if (params.output == 'macros') {
return this.macros;
} else {
return output;
}
};
Twig.Template.prototype.importFile = function(file) {
var url, sub_template;
if (!this.url && this.options.allowInlineIncludes) {
file = this.path ? this.path + '/' + file : file;
sub_template = Twig.Templates.load(file);
if (!sub_template) {
sub_template = Twig.Templates.loadRemote(url, {
id: file,
method: this.getLoaderMethod(),
async: false,
options: this.options
});
if (!sub_template) {
throw new Twig.Error("Unable to find the template " + file);
}
}
sub_template.options = this.options;
return sub_template;
}
url = Twig.path.parsePath(this, file);
// Load blocks from an external file
sub_template = Twig.Templates.loadRemote(url, {
method: this.getLoaderMethod(),
base: this.base,
async: false,
options: this.options,
id: url
});
return sub_template;
};
Twig.Template.prototype.importBlocks = function(file, override) {
var sub_template = this.importFile(file),
context = this.context,
that = this,
key;
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);
}
});
};
Twig.Template.prototype.importMacros = function(file) {
var url = Twig.path.parsePath(this, file);
// load remote template
var remoteTemplate = Twig.Templates.loadRemote(url, {
method: this.getLoaderMethod(),
async: false,
id: url
});
return remoteTemplate;
};
Twig.Template.prototype.getLoaderMethod = function() {
if (this.path) {
return 'fs';
}
if (this.url) {
return 'ajax';
}
return this.method || 'fs';
};
Twig.Template.prototype.compile = function(options) {
// compile the template into raw JS
return Twig.compiler.compile(this, options);
};
/**
* 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 (typeo