twig.core.js | |
---|---|
| var Twig = (function (Twig) {
"use strict"; |
twig.core.jsThis file handles template level tokenizing, compiling and parsing. | Twig.trace = false;
Twig.debug = false; |
Default caching to true for the improved performance it offers | Twig.cache = true;
Twig.placeholders = {
parent: "{{|PARENT|}}"
};
/**
* 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() {
return this.name + ": " + this.message;
};
/**
* 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));}}
};
/**
* 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'
};
/**
* Token syntax definitions.
*/
Twig.token.definitions = { |
Output type tokens These typically take the form | output: {
type: Twig.token.type.output,
open: '{{',
close: '}}'
}, |
Logic type tokens These typically take a form like | logic: {
type: Twig.token.type.logic,
open: '{%',
close: '%}'
}, |
Comment type tokens These take the form | comment: {
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
},
token_type,
token_template,
first_key_position;
for (token_type in Twig.token.definitions) {
if (Twig.token.definitions.hasOwnProperty(token_type)) {
token_template = Twig.token.definitions[token_type];
first_key_position = template.indexOf(token_template.open);
Twig.log.trace("Twig.token.findStart: ", "Searching for ", token_template.open, " found at ", first_key_position); |
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;
}
}
}
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);
}
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()
});
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) { |
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, |
The previous token's template | prev_template = null, |
The output token | tok_output = null, |
Logic Token values | type = null,
open = null,
next = null;
while (tokens.length > 0) {
token = tokens.shift();
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 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 (prev_template.next.indexOf(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);
}
}
break; |
Do nothing, comments should be ignored | case Twig.token.type.comment:
break;
case Twig.token.type.output:
Twig.expression.compile.apply(this, [token]);
if (stack.length > 0) {
intermediate_output.push(token);
} else {
output.push(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;
};
/**
* 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) {
var output = [], |
Track logic chains | chain = true,
that = this; |
Default to an empty object if none provided | context = context || { };
tokens.forEach(function (token) {
Twig.log.debug("Twig.parse: ", "Parsing token: ", token);
switch (token.type) {
case Twig.token.type.raw:
output.push(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;
case Twig.token.type.output: |
Parse the given expression in the given context | output.push(Twig.expression.parse.apply(that, [token.stack, context]));
break;
}
});
return output.join("");
};
/**
* 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;
}; |
Namespace for template storage and retrieval | Twig.Templates = {
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.Templates.registry.hasOwnProperty(id)) {
throw new Twig.Error("There is already a template with the ID " + id);
}
return true;
}
/**
* 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)
* 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 id = params.id,
method = params.method,
async = params.async,
precompiled = params.precompiled,
options = params.options,
template = null; |
Default to async | if (async === undefined) async = true; |
Default to the URL so the template is cached. | if (id === undefined) {
id = location;
} |
Check for existing template | if (Twig.cache && Twig.Templates.registry.hasOwnProperty(id)) { |
A template is already saved with the given id. | if (callback) {
callback(Twig.Templates.registry[id]);
}
return Twig.Templates.registry[id];
}
if (method == 'ajax') {
if (typeof XMLHttpRequest == "undefined") {
throw new Error("Unsupported platform: Unable to do remote requests " +
"because there is no XMLHTTPRequest implementation");
}
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() {
var data = null;
if(xmlhttp.readyState == 4) {
Twig.log.debug("Got template ", xmlhttp.responseText);
if (precompiled === true) {
data = JSON.parse(xmlhttp.responseText);
} else {
data = xmlhttp.responseText;
}
template = new Twig.Template({
data: data,
id: id,
url: location,
options: options
});
if (callback) {
callback(template);
}
}
};
xmlhttp.open("GET", location, async);
xmlhttp.send();
} else { // if method = 'fs' |
Create local scope | (function() {
var fs = require('fs'),
data = null;
if (async === true) { |
async with callback | fs.readFile(location, 'utf8', function(err, data) {
if (err) {
if (error_callback) {
error_callback(err);
}
return;
}
if (precompiled === true) {
data = JSON.parse(data);
} |
template is in data | template = new Twig.Template({
data: data,
id: id,
path: location,
options: options
});
if (callback) {
callback(template);
}
});
} else {
data = fs.readFileSync(location, 'utf8');
if (precompiled === true) {
data = JSON.parse(data);
} |
sync | template = new Twig.Template({
data: data,
id: id,
path: location,
options: options
});
if (callback) {
callback(template);
}
}
})();
}
if (async === false) {
return template;
} else { |
placeholder for now, should eventually return a deferred object. | return true;
}
}; |
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,
path = params.path,
url = params.url, |
parser options | options = params.options; |
What is stored in a Twig.TemplateThe Twig Template hold several chucks of data. | this.id = id;
this.path = path;
this.url = url;
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.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;
}
output = Twig.parse.apply(this, [this.tokens, this.context]); |
Does this template extend another | if (this.extend) {
url = relativePath(this, this.extend); |
This template extends another, load it with this template's blocks | this.parent = Twig.Templates.loadRemote(url, {
method: this.url?'ajax':'fs',
async: false,
id: url
});
return this.parent.render(this.context, {
blocks: this.blocks
});
}
if (params.output == 'blocks') {
return this.blocks;
} else {
return output;
}
};
Twig.Template.prototype.importFile = function(file) {
var url = relativePath(this, file), |
Load blocks from an external file | sub_template = Twig.Templates.loadRemote(url, {
method: this.url?'ajax':'fs',
async: false,
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 | Object.keys(sub_template.blocks).forEach(function(key) {
if (override || that.blocks[key] === undefined) {
that.blocks[key] = sub_template.blocks[key];
}
});
};
Twig.Template.prototype.compile = function(options) { |
compile the template into raw JS | return Twig.compiler.compile(this, options);
};
/**
* Generate the relative canonical version of a url based on the given base path and file path.
*
* @param {string} template The Twig.Template.
* @param {string} file The file path, relative to the base path.
*
* @return {string} The canonical version of the path.
*/
function relativePath(template, file) {
var base,
base_path,
sep_chr = '/',
new_path = [],
val;
if (template.url) {
base = template.url;
} else if (template.path) {
base = template.path;
} else {
throw new Twig.Error("Cannot extend an inline template.");
}
base_path = base.split(sep_chr), |
Remove file from url | base_path.pop();
base_path = base_path.concat(file.split(sep_chr));
while (base_path.length > 0) {
val = base_path.shift();
if (val == ".") { |
Ignore | } else if (val == ".." && new_path.length > 0 && new_path[new_path.length-1] != "..") {
new_path.pop();
} else {
new_path.push(val);
}
}
return new_path.join(sep_chr);
}
return Twig;
}) (Twig || { });
|