todomvc
Version:
> Helping you select an MV\* framework
1,858 lines (1,570 loc) • 139 kB
JavaScript
// Browser bundle of nunjucks 1.0.0
(function() {
var modules = {};
(function() {
// A simple class system, more documentation to come
function extend(cls, name, props) {
// This does that same thing as Object.create, but with support for IE8
var F = function() {};
F.prototype = cls.prototype;
var prototype = new F();
var fnTest = /xyz/.test(function(){ xyz; }) ? /\bparent\b/ : /.*/;
props = props || {};
for(var k in props) {
var src = props[k];
var parent = prototype[k];
if(typeof parent == "function" &&
typeof src == "function" &&
fnTest.test(src)) {
prototype[k] = (function (src, parent) {
return function() {
// Save the current parent method
var tmp = this.parent;
// Set parent to the previous method, call, and restore
this.parent = parent;
var res = src.apply(this, arguments);
this.parent = tmp;
return res;
};
})(src, parent);
}
else {
prototype[k] = src;
}
}
prototype.typename = name;
var new_cls = function() {
if(prototype.init) {
prototype.init.apply(this, arguments);
}
};
new_cls.prototype = prototype;
new_cls.prototype.constructor = new_cls;
new_cls.extend = function(name, props) {
if(typeof name == "object") {
props = name;
name = "anonymous";
}
return extend(new_cls, name, props);
};
return new_cls;
}
modules['object'] = extend(Object, "Object", {});
})();
(function() {
var ArrayProto = Array.prototype;
var ObjProto = Object.prototype;
var escapeMap = {
'&': '&',
'"': '"',
"'": ''',
"<": '<',
">": '>'
};
var lookupEscape = function(ch) {
return escapeMap[ch];
};
var exports = modules['lib'] = {};
exports.withPrettyErrors = function(path, withInternals, func) {
try {
return func();
} catch (e) {
if (!e.Update) {
// not one of ours, cast it
e = new exports.TemplateError(e);
}
e.Update(path);
// Unless they marked the dev flag, show them a trace from here
if (!withInternals) {
var old = e;
e = new Error(old.message);
e.name = old.name;
}
throw e;
}
};
exports.TemplateError = function(message, lineno, colno) {
var err = this;
if (message instanceof Error) { // for casting regular js errors
err = message;
message = message.name + ": " + message.message;
} else {
if(Error.captureStackTrace) {
Error.captureStackTrace(err);
}
}
err.name = 'Template render error';
err.message = message;
err.lineno = lineno;
err.colno = colno;
err.firstUpdate = true;
err.Update = function(path) {
var message = "(" + (path || "unknown path") + ")";
// only show lineno + colno next to path of template
// where error occurred
if (this.firstUpdate) {
if(this.lineno && this.colno) {
message += ' [Line ' + this.lineno + ', Column ' + this.colno + ']';
}
else if(this.lineno) {
message += ' [Line ' + this.lineno + ']';
}
}
message += '\n ';
if (this.firstUpdate) {
message += ' ';
}
this.message = message + (this.message || '');
this.firstUpdate = false;
return this;
};
return err;
};
exports.TemplateError.prototype = Error.prototype;
exports.escape = function(val) {
return val.replace(/[&"'<>]/g, lookupEscape);
};
exports.isFunction = function(obj) {
return ObjProto.toString.call(obj) == '[object Function]';
};
exports.isArray = Array.isArray || function(obj) {
return ObjProto.toString.call(obj) == '[object Array]';
};
exports.isString = function(obj) {
return ObjProto.toString.call(obj) == '[object String]';
};
exports.isObject = function(obj) {
return obj === Object(obj);
};
exports.groupBy = function(obj, val) {
var result = {};
var iterator = exports.isFunction(val) ? val : function(obj) { return obj[val]; };
for(var i=0; i<obj.length; i++) {
var value = obj[i];
var key = iterator(value, i);
(result[key] || (result[key] = [])).push(value);
}
return result;
};
exports.toArray = function(obj) {
return Array.prototype.slice.call(obj);
};
exports.without = function(array) {
var result = [];
if (!array) {
return result;
}
var index = -1,
length = array.length,
contains = exports.toArray(arguments).slice(1);
while(++index < length) {
if(contains.indexOf(array[index]) === -1) {
result.push(array[index]);
}
}
return result;
};
exports.extend = function(obj, obj2) {
for(var k in obj2) {
obj[k] = obj2[k];
}
return obj;
};
exports.repeat = function(char_, n) {
var str = '';
for(var i=0; i<n; i++) {
str += char_;
}
return str;
};
exports.each = function(obj, func, context) {
if(obj == null) {
return;
}
if(ArrayProto.each && obj.each == ArrayProto.each) {
obj.forEach(func, context);
}
else if(obj.length === +obj.length) {
for(var i=0, l=obj.length; i<l; i++) {
func.call(context, obj[i], i, obj);
}
}
};
exports.map = function(obj, func) {
var results = [];
if(obj == null) {
return results;
}
if(ArrayProto.map && obj.map === ArrayProto.map) {
return obj.map(func);
}
for(var i=0; i<obj.length; i++) {
results[results.length] = func(obj[i], i);
}
if(obj.length === +obj.length) {
results.length = obj.length;
}
return results;
};
exports.asyncParallel = function(funcs, done) {
var count = funcs.length,
result = new Array(count),
current = 0;
var makeNext = function(i) {
return function(res) {
result[i] = res;
current += 1;
if (current === count) {
done(result);
}
};
};
for (var i = 0; i < count; i++) {
funcs[i](makeNext(i));
}
};
exports.asyncIter = function(arr, iter, cb) {
var i = -1;
function next() {
i++;
if(i < arr.length) {
iter(arr[i], i, next, cb);
}
else {
cb();
}
}
next();
};
exports.asyncFor = function(obj, iter, cb) {
var keys = exports.keys(obj);
var len = keys.length;
var i = -1;
function next() {
i++;
var k = keys[i];
if(i < len) {
iter(k, obj[k], i, len, next);
}
else {
cb();
}
}
next();
};
if(!Array.prototype.indexOf) {
Array.prototype.indexOf = function(array, searchElement /*, fromIndex */) {
if (array == null) {
throw new TypeError();
}
var t = Object(array);
var len = t.length >>> 0;
if (len === 0) {
return -1;
}
var n = 0;
if (arguments.length > 2) {
n = Number(arguments[2]);
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) {
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;
}
}
return -1;
};
}
if(!Array.prototype.map) {
Array.prototype.map = function() {
throw new Error("map is unimplemented for this js engine");
};
}
exports.keys = function(obj) {
if(Object.prototype.keys) {
return obj.keys();
}
else {
var keys = [];
for(var k in obj) {
if(obj.hasOwnProperty(k)) {
keys.push(k);
}
}
return keys;
}
}
})();
(function() {
var util = modules["util"];
var lib = modules["lib"];
var Object = modules["object"];
function traverseAndCheck(obj, type, results) {
if(obj instanceof type) {
results.push(obj);
}
if(obj instanceof Node) {
obj.findAll(type, results);
}
}
var Node = Object.extend("Node", {
init: function(lineno, colno) {
this.lineno = lineno;
this.colno = colno;
var fields = this.fields;
for(var i=0, l=fields.length; i<l; i++) {
var field = fields[i];
// The first two args are line/col numbers, so offset by 2
var val = arguments[i + 2];
// Fields should never be undefined, but null. It makes
// testing easier to normalize values.
if(val === undefined) {
val = null;
}
this[field] = val;
}
},
findAll: function(type, results) {
results = results || [];
if(this instanceof NodeList) {
var children = this.children;
for(var i=0, l=children.length; i<l; i++) {
traverseAndCheck(children[i], type, results);
}
}
else {
var fields = this.fields;
for(var i=0, l=fields.length; i<l; i++) {
traverseAndCheck(this[fields[i]], type, results);
}
}
return results;
},
iterFields: function(func) {
lib.each(this.fields, function(field) {
func(this[field], field);
}, this);
}
});
// Abstract nodes
var Value = Node.extend("Value", { fields: ['value'] });
// Concrete nodes
var NodeList = Node.extend("NodeList", {
fields: ['children'],
init: function(lineno, colno, nodes) {
this.parent(lineno, colno, nodes || []);
},
addChild: function(node) {
this.children.push(node);
}
});
var Root = NodeList.extend("Root");
var Literal = Value.extend("Literal");
var Symbol = Value.extend("Symbol");
var Group = NodeList.extend("Group");
var Array = NodeList.extend("Array");
var Pair = Node.extend("Pair", { fields: ['key', 'value'] });
var Dict = NodeList.extend("Dict");
var LookupVal = Node.extend("LookupVal", { fields: ['target', 'val'] });
var If = Node.extend("If", { fields: ['cond', 'body', 'else_'] });
var IfAsync = If.extend("IfAsync");
var InlineIf = Node.extend("InlineIf", { fields: ['cond', 'body', 'else_'] });
var For = Node.extend("For", { fields: ['arr', 'name', 'body'] });
var AsyncEach = For.extend("AsyncEach");
var AsyncAll = For.extend("AsyncAll");
var Macro = Node.extend("Macro", { fields: ['name', 'args', 'body'] });
var Import = Node.extend("Import", { fields: ['template', 'target'] });
var FromImport = Node.extend("FromImport", {
fields: ['template', 'names'],
init: function(lineno, colno, template, names) {
this.parent(lineno, colno,
template,
names || new NodeList());
}
});
var FunCall = Node.extend("FunCall", { fields: ['name', 'args'] });
var Filter = FunCall.extend("Filter");
var FilterAsync = Filter.extend("FilterAsync", {
fields: ['name', 'args', 'symbol']
});
var KeywordArgs = Dict.extend("KeywordArgs");
var Block = Node.extend("Block", { fields: ['name', 'body'] });
var Super = Node.extend("Super", { fields: ['blockName', 'symbol'] });
var TemplateRef = Node.extend("TemplateRef", { fields: ['template'] });
var Extends = TemplateRef.extend("Extends");
var Include = TemplateRef.extend("Include");
var Set = Node.extend("Set", { fields: ['targets', 'value'] });
var Output = NodeList.extend("Output");
var TemplateData = Literal.extend("TemplateData");
var UnaryOp = Node.extend("UnaryOp", { fields: ['target'] });
var BinOp = Node.extend("BinOp", { fields: ['left', 'right'] });
var Or = BinOp.extend("Or");
var And = BinOp.extend("And");
var Not = UnaryOp.extend("Not");
var Add = BinOp.extend("Add");
var Sub = BinOp.extend("Sub");
var Mul = BinOp.extend("Mul");
var Div = BinOp.extend("Div");
var FloorDiv = BinOp.extend("FloorDiv");
var Mod = BinOp.extend("Mod");
var Pow = BinOp.extend("Pow");
var Neg = UnaryOp.extend("Neg");
var Pos = UnaryOp.extend("Pos");
var Compare = Node.extend("Compare", { fields: ['expr', 'ops'] });
var CompareOperand = Node.extend("CompareOperand", {
fields: ['expr', 'type']
});
var CustomTag = Node.extend("CustomTag", {
init: function(lineno, colno, name) {
this.lineno = lineno;
this.colno = colno;
this.name = name;
}
});
var CallExtension = Node.extend("CallExtension", {
fields: ['extName', 'prop', 'args', 'contentArgs'],
init: function(ext, prop, args, contentArgs) {
this.extName = ext._name || ext;
this.prop = prop;
this.args = args || new NodeList();
this.contentArgs = contentArgs || [];
}
});
var CallExtensionAsync = CallExtension.extend("CallExtensionAsync");
// Print the AST in a nicely formatted tree format for debuggin
function printNodes(node, indent) {
indent = indent || 0;
// This is hacky, but this is just a debugging function anyway
function print(str, indent, inline) {
var lines = str.split("\n");
for(var i=0; i<lines.length; i++) {
if(lines[i]) {
if((inline && i > 0) || !inline) {
for(var j=0; j<indent; j++) {
util.print(" ");
}
}
}
if(i === lines.length-1) {
util.print(lines[i]);
}
else {
util.puts(lines[i]);
}
}
}
print(node.typename + ": ", indent);
if(node instanceof NodeList) {
print('\n');
lib.each(node.children, function(n) {
printNodes(n, indent + 2);
});
}
else if(node instanceof CallExtension) {
print(node.extName + '.' + node.prop);
print('\n');
if(node.args) {
printNodes(node.args, indent + 2);
}
if(node.contentArgs) {
lib.each(node.contentArgs, function(n) {
printNodes(n, indent + 2);
});
}
}
else {
var nodes = null;
var props = null;
node.iterFields(function(val, field) {
if(val instanceof Node) {
nodes = nodes || {};
nodes[field] = val;
}
else {
props = props || {};
props[field] = val;
}
});
if(props) {
print(util.inspect(props, true, null) + '\n', null, true);
}
else {
print('\n');
}
if(nodes) {
for(var k in nodes) {
printNodes(nodes[k], indent + 2);
}
}
}
}
// var t = new NodeList(0, 0,
// [new Value(0, 0, 3),
// new Value(0, 0, 10),
// new Pair(0, 0,
// new Value(0, 0, 'key'),
// new Value(0, 0, 'value'))]);
// printNodes(t);
modules['nodes'] = {
Node: Node,
Root: Root,
NodeList: NodeList,
Value: Value,
Literal: Literal,
Symbol: Symbol,
Group: Group,
Array: Array,
Pair: Pair,
Dict: Dict,
Output: Output,
TemplateData: TemplateData,
If: If,
IfAsync: IfAsync,
InlineIf: InlineIf,
For: For,
AsyncEach: AsyncEach,
AsyncAll: AsyncAll,
Macro: Macro,
Import: Import,
FromImport: FromImport,
FunCall: FunCall,
Filter: Filter,
FilterAsync: FilterAsync,
KeywordArgs: KeywordArgs,
Block: Block,
Super: Super,
Extends: Extends,
Include: Include,
Set: Set,
LookupVal: LookupVal,
BinOp: BinOp,
Or: Or,
And: And,
Not: Not,
Add: Add,
Sub: Sub,
Mul: Mul,
Div: Div,
FloorDiv: FloorDiv,
Mod: Mod,
Pow: Pow,
Neg: Neg,
Pos: Pos,
Compare: Compare,
CompareOperand: CompareOperand,
CallExtension: CallExtension,
CallExtensionAsync: CallExtensionAsync,
printNodes: printNodes
};
})();
(function() {
var lib = modules["lib"];
var Object = modules["object"];
// Frames keep track of scoping both at compile-time and run-time so
// we know how to access variables. Block tags can introduce special
// variables, for example.
var Frame = Object.extend({
init: function(parent) {
this.variables = {};
this.parent = parent;
},
set: function(name, val) {
// Allow variables with dots by automatically creating the
// nested structure
var parts = name.split('.');
var obj = this.variables;
for(var i=0; i<parts.length - 1; i++) {
var id = parts[i];
if(!obj[id]) {
obj[id] = {};
}
obj = obj[id];
}
obj[parts[parts.length - 1]] = val;
},
get: function(name) {
var val = this.variables[name];
if(val !== undefined && val !== null) {
return val;
}
return null;
},
lookup: function(name) {
var p = this.parent;
var val = this.variables[name];
if(val !== undefined && val !== null) {
return val;
}
return p && p.lookup(name);
},
push: function() {
return new Frame(this);
},
pop: function() {
return this.parent;
}
});
function makeMacro(argNames, kwargNames, func) {
return function() {
var argCount = numArgs(arguments);
var args;
var kwargs = getKeywordArgs(arguments);
if(argCount > argNames.length) {
args = Array.prototype.slice.call(arguments, 0, argNames.length);
// Positional arguments that should be passed in as
// keyword arguments (essentially default values)
var vals = Array.prototype.slice.call(arguments, args.length, argCount);
for(var i=0; i<vals.length; i++) {
if(i < kwargNames.length) {
kwargs[kwargNames[i]] = vals[i];
}
}
args.push(kwargs);
}
else if(argCount < argNames.length) {
args = Array.prototype.slice.call(arguments, 0, argCount);
for(var i=argCount; i<argNames.length; i++) {
var arg = argNames[i];
// Keyword arguments that should be passed as
// positional arguments, i.e. the caller explicitly
// used the name of a positional arg
args.push(kwargs[arg]);
delete kwargs[arg];
}
args.push(kwargs);
}
else {
args = arguments;
}
return func.apply(this, args);
};
}
function makeKeywordArgs(obj) {
obj.__keywords = true;
return obj;
}
function getKeywordArgs(args) {
var len = args.length;
if(len) {
var lastArg = args[len - 1];
if(lastArg && lastArg.hasOwnProperty('__keywords')) {
return lastArg;
}
}
return {};
}
function numArgs(args) {
var len = args.length;
if(len === 0) {
return 0;
}
var lastArg = args[len - 1];
if(lastArg && lastArg.hasOwnProperty('__keywords')) {
return len - 1;
}
else {
return len;
}
}
// A SafeString object indicates that the string should not be
// autoescaped. This happens magically because autoescaping only
// occurs on primitive string objects.
function SafeString(val) {
if(typeof val != 'string') {
return val;
}
this.toString = function() {
return val;
};
this.length = val.length;
var methods = [
'charAt', 'charCodeAt', 'concat', 'contains',
'endsWith', 'fromCharCode', 'indexOf', 'lastIndexOf',
'length', 'localeCompare', 'match', 'quote', 'replace',
'search', 'slice', 'split', 'startsWith', 'substr',
'substring', 'toLocaleLowerCase', 'toLocaleUpperCase',
'toLowerCase', 'toUpperCase', 'trim', 'trimLeft', 'trimRight'
];
for(var i=0; i<methods.length; i++) {
this[methods[i]] = markSafe(val[methods[i]]);
}
}
function copySafeness(dest, target) {
if(dest instanceof SafeString) {
return new SafeString(target);
}
return target.toString();
}
function markSafe(val) {
var type = typeof val;
if(type === 'string') {
return new SafeString(val);
}
else if(type !== 'function') {
return val;
}
else {
return function() {
var ret = val.apply(this, arguments);
if(typeof ret === 'string') {
return new SafeString(ret);
}
return ret;
};
}
}
function suppressValue(val, autoescape) {
val = (val !== undefined && val !== null) ? val : "";
if(autoescape && typeof val === "string") {
val = lib.escape(val);
}
return val;
}
function memberLookup(obj, val) {
obj = obj || {};
if(typeof obj[val] === 'function') {
return function() {
return obj[val].apply(obj, arguments);
};
}
return obj[val];
}
function callWrap(obj, name, args) {
if(!obj) {
throw new Error('Unable to call `' + name + '`, which is undefined or falsey');
}
else if(typeof obj !== 'function') {
throw new Error('Unable to call `' + name + '`, which is not a function');
}
return obj.apply(this, args);
}
function contextOrFrameLookup(context, frame, name) {
var val = frame.lookup(name);
return (val !== undefined && val !== null) ?
val :
context.lookup(name);
}
function handleError(error, lineno, colno) {
if(error.lineno) {
return error;
}
else {
return new lib.TemplateError(error, lineno, colno);
}
}
function asyncEach(arr, dimen, iter, cb) {
if(lib.isArray(arr)) {
var len = arr.length;
lib.asyncIter(arr, function(item, i, next) {
switch(dimen) {
case 1: iter(item, i, len, next); break;
case 2: iter(item[0], item[1], i, len, next); break;
case 3: iter(item[0], item[1], item[2], i, len, next); break;
default:
item.push(i, next);
iter.apply(this, item);
}
}, cb);
}
else {
lib.asyncFor(arr, function(key, val, i, len, next) {
iter(key, val, i, len, next);
}, cb);
}
}
function asyncAll(arr, dimen, func, cb) {
var finished = 0;
var len;
var outputArr;
function done(i, output) {
finished++;
outputArr[i] = output;
if(finished == len) {
cb(null, outputArr.join(''));
}
}
if(lib.isArray(arr)) {
len = arr.length;
outputArr = new Array(len);
if(len == 0) {
cb(null, '');
}
else {
for(var i=0; i<arr.length; i++) {
var item = arr[i];
switch(dimen) {
case 1: func(item, i, len, done); break;
case 2: func(item[0], item[1], i, len, done); break;
case 3: func(item[0], item[1], item[2], i, len, done); break;
default:
item.push(i, done);
func.apply(this, item);
}
}
}
}
else {
var keys = lib.keys(arr);
len = keys.length;
outputArr = new Array(len);
if(len == 0) {
cb(null, '');
}
else {
for(var i=0; i<keys.length; i++) {
var k = keys[i];
func(k, arr[k], i, len, done);
}
}
}
}
modules['runtime'] = {
Frame: Frame,
makeMacro: makeMacro,
makeKeywordArgs: makeKeywordArgs,
numArgs: numArgs,
suppressValue: suppressValue,
memberLookup: memberLookup,
contextOrFrameLookup: contextOrFrameLookup,
callWrap: callWrap,
handleError: handleError,
isArray: lib.isArray,
asyncEach: lib.asyncEach,
keys: lib.keys,
SafeString: SafeString,
copySafeness: copySafeness,
markSafe: markSafe,
asyncEach: asyncEach,
asyncAll: asyncAll
};
})();
(function() {
var lib = modules["lib"];
var whitespaceChars = " \n\t\r";
var delimChars = "()[]{}%*-+/#,:|.<>=!";
var intChars = "0123456789";
var BLOCK_START = "{%";
var BLOCK_END = "%}";
var VARIABLE_START = "{{";
var VARIABLE_END = "}}";
var COMMENT_START = "{#";
var COMMENT_END = "#}";
var TOKEN_STRING = "string";
var TOKEN_WHITESPACE = "whitespace";
var TOKEN_DATA = "data";
var TOKEN_BLOCK_START = "block-start";
var TOKEN_BLOCK_END = "block-end";
var TOKEN_VARIABLE_START = "variable-start";
var TOKEN_VARIABLE_END = "variable-end";
var TOKEN_COMMENT = "comment";
var TOKEN_LEFT_PAREN = "left-paren";
var TOKEN_RIGHT_PAREN = "right-paren";
var TOKEN_LEFT_BRACKET = "left-bracket";
var TOKEN_RIGHT_BRACKET = "right-bracket";
var TOKEN_LEFT_CURLY = "left-curly";
var TOKEN_RIGHT_CURLY = "right-curly";
var TOKEN_OPERATOR = "operator";
var TOKEN_COMMA = "comma";
var TOKEN_COLON = "colon";
var TOKEN_PIPE = "pipe";
var TOKEN_INT = "int";
var TOKEN_FLOAT = "float";
var TOKEN_BOOLEAN = "boolean";
var TOKEN_SYMBOL = "symbol";
var TOKEN_SPECIAL = "special";
function token(type, value, lineno, colno) {
return {
type: type,
value: value,
lineno: lineno,
colno: colno
};
}
function Tokenizer(str) {
this.str = str;
this.index = 0;
this.len = str.length;
this.lineno = 0;
this.colno = 0;
this.in_code = false;
}
Tokenizer.prototype.nextToken = function() {
var lineno = this.lineno;
var colno = this.colno;
if(this.in_code) {
// Otherwise, if we are in a block parse it as code
var cur = this.current();
var tok;
if(this.is_finished()) {
// We have nothing else to parse
return null;
}
else if(cur == "\"" || cur == "'") {
// We've hit a string
return token(TOKEN_STRING, this.parseString(cur), lineno, colno);
}
else if((tok = this._extract(whitespaceChars))) {
// We hit some whitespace
return token(TOKEN_WHITESPACE, tok, lineno, colno);
}
else if((tok = this._extractString(BLOCK_END)) ||
(tok = this._extractString('-' + BLOCK_END))) {
// Special check for the block end tag
//
// It is a requirement that start and end tags are composed of
// delimiter characters (%{}[] etc), and our code always
// breaks on delimiters so we can assume the token parsing
// doesn't consume these elsewhere
this.in_code = false;
return token(TOKEN_BLOCK_END, tok, lineno, colno);
}
else if((tok = this._extractString(VARIABLE_END))) {
// Special check for variable end tag (see above)
this.in_code = false;
return token(TOKEN_VARIABLE_END, tok, lineno, colno);
}
else if(delimChars.indexOf(cur) != -1) {
// We've hit a delimiter (a special char like a bracket)
this.forward();
var complexOps = ['==', '!=', '<=', '>=', '//', '**'];
var curComplex = cur + this.current();
var type;
if(complexOps.indexOf(curComplex) !== -1) {
this.forward();
cur = curComplex;
}
switch(cur) {
case "(": type = TOKEN_LEFT_PAREN; break;
case ")": type = TOKEN_RIGHT_PAREN; break;
case "[": type = TOKEN_LEFT_BRACKET; break;
case "]": type = TOKEN_RIGHT_BRACKET; break;
case "{": type = TOKEN_LEFT_CURLY; break;
case "}": type = TOKEN_RIGHT_CURLY; break;
case ",": type = TOKEN_COMMA; break;
case ":": type = TOKEN_COLON; break;
case "|": type = TOKEN_PIPE; break;
default: type = TOKEN_OPERATOR;
}
return token(type, cur, lineno, colno);
}
else {
// We are not at whitespace or a delimiter, so extract the
// text and parse it
tok = this._extractUntil(whitespaceChars + delimChars);
if(tok.match(/^[-+]?[0-9]+$/)) {
if(this.current() == '.') {
this.forward();
var dec = this._extract(intChars);
return token(TOKEN_FLOAT, tok + '.' + dec, lineno, colno);
}
else {
return token(TOKEN_INT, tok, lineno, colno);
}
}
else if(tok.match(/^(true|false)$/)) {
return token(TOKEN_BOOLEAN, tok, lineno, colno);
}
else if(tok) {
return token(TOKEN_SYMBOL, tok, lineno, colno);
}
else {
throw new Error("Unexpected value while parsing: " + tok);
}
}
}
else {
// Parse out the template text, breaking on tag
// delimiters because we need to look for block/variable start
// tags (don't use the full delimChars for optimization)
var beginChars = (BLOCK_START.charAt(0) +
VARIABLE_START.charAt(0) +
COMMENT_START.charAt(0) +
COMMENT_END.charAt(0));
var tok;
if(this.is_finished()) {
return null;
}
else if((tok = this._extractString(BLOCK_START + '-')) ||
(tok = this._extractString(BLOCK_START))) {
this.in_code = true;
return token(TOKEN_BLOCK_START, tok, lineno, colno);
}
else if((tok = this._extractString(VARIABLE_START))) {
this.in_code = true;
return token(TOKEN_VARIABLE_START, tok, lineno, colno);
}
else {
tok = '';
var data;
var in_comment = false;
if(this._matches(COMMENT_START)) {
in_comment = true;
tok = this._extractString(COMMENT_START);
}
// Continually consume text, breaking on the tag delimiter
// characters and checking to see if it's a start tag.
//
// We could hit the end of the template in the middle of
// our looping, so check for the null return value from
// _extractUntil
while((data = this._extractUntil(beginChars)) !== null) {
tok += data;
if((this._matches(BLOCK_START) ||
this._matches(VARIABLE_START) ||
this._matches(COMMENT_START)) &&
!in_comment) {
// If it is a start tag, stop looping
break;
}
else if(this._matches(COMMENT_END)) {
if(!in_comment) {
throw new Error("unexpected end of comment");
}
tok += this._extractString(COMMENT_END);
break;
}
else {
// It does not match any tag, so add the character and
// carry on
tok += this.current();
this.forward();
}
}
if(data === null && in_comment) {
throw new Error("expected end of comment, got end of file");
}
return token(in_comment ? TOKEN_COMMENT : TOKEN_DATA,
tok,
lineno,
colno);
}
}
throw new Error("Could not parse text");
};
Tokenizer.prototype.parseString = function(delimiter) {
this.forward();
var lineno = this.lineno;
var colno = this.colno;
var str = "";
while(!this.is_finished() && this.current() != delimiter) {
var cur = this.current();
if(cur == "\\") {
this.forward();
switch(this.current()) {
case "n": str += "\n"; break;
case "t": str += "\t"; break;
case "r": str += "\r"; break;
default:
str += this.current();
}
this.forward();
}
else {
str += cur;
this.forward();
}
}
this.forward();
return str;
};
Tokenizer.prototype._matches = function(str) {
if(this.index + str.length > this.length) {
return null;
}
var m = this.str.slice(this.index, this.index + str.length);
return m == str;
};
Tokenizer.prototype._extractString = function(str) {
if(this._matches(str)) {
this.index += str.length;
return str;
}
return null;
};
Tokenizer.prototype._extractUntil = function(charString) {
// Extract all non-matching chars, with the default matching set
// to everything
return this._extractMatching(true, charString || "");
};
Tokenizer.prototype._extract = function(charString) {
// Extract all matching chars (no default, so charString must be
// explicit)
return this._extractMatching(false, charString);
};
Tokenizer.prototype._extractMatching = function (breakOnMatch, charString) {
// Pull out characters until a breaking char is hit.
// If breakOnMatch is false, a non-matching char stops it.
// If breakOnMatch is true, a matching char stops it.
if(this.is_finished()) {
return null;
}
var first = charString.indexOf(this.current());
// Only proceed if the first character doesn't meet our condition
if((breakOnMatch && first == -1) ||
(!breakOnMatch && first != -1)) {
var t = this.current();
this.forward();
// And pull out all the chars one at a time until we hit a
// breaking char
var idx = charString.indexOf(this.current());
while(((breakOnMatch && idx == -1) ||
(!breakOnMatch && idx != -1)) && !this.is_finished()) {
t += this.current();
this.forward();
idx = charString.indexOf(this.current());
}
return t;
}
return "";
};
Tokenizer.prototype.is_finished = function() {
return this.index >= this.len;
};
Tokenizer.prototype.forwardN = function(n) {
for(var i=0; i<n; i++) {
this.forward();
}
};
Tokenizer.prototype.forward = function() {
this.index++;
if(this.previous() == "\n") {
this.lineno++;
this.colno = 0;
}
else {
this.colno++;
}
};
Tokenizer.prototype.backN = function(n) {
for(var i=0; i<n; i++) {
self.back();
}
};
Tokenizer.prototype.back = function() {
this.index--;
if(this.current() == "\n") {
this.lineno--;
var idx = this.src.lastIndexOf("\n", this.index-1);
if(idx == -1) {
this.colno = this.index;
}
else {
this.colno = this.index - idx;
}
}
else {
this.colno--;
}
};
Tokenizer.prototype.current = function() {
if(!this.is_finished()) {
return this.str.charAt(this.index);
}
return "";
};
Tokenizer.prototype.previous = function() {
return this.str.charAt(this.index-1);
};
modules['lexer'] = {
lex: function(src) {
return new Tokenizer(src);
},
setTags: function(tags) {
BLOCK_START = tags.blockStart || BLOCK_START;
BLOCK_END = tags.blockEnd || BLOCK_END;
VARIABLE_START = tags.variableStart || VARIABLE_START;
VARIABLE_END = tags.variableEnd || VARIABLE_END;
COMMENT_START = tags.commentStart || COMMENT_START;
COMMENT_END = tags.commentEnd || COMMENT_END;
},
TOKEN_STRING: TOKEN_STRING,
TOKEN_WHITESPACE: TOKEN_WHITESPACE,
TOKEN_DATA: TOKEN_DATA,
TOKEN_BLOCK_START: TOKEN_BLOCK_START,
TOKEN_BLOCK_END: TOKEN_BLOCK_END,
TOKEN_VARIABLE_START: TOKEN_VARIABLE_START,
TOKEN_VARIABLE_END: TOKEN_VARIABLE_END,
TOKEN_COMMENT: TOKEN_COMMENT,
TOKEN_LEFT_PAREN: TOKEN_LEFT_PAREN,
TOKEN_RIGHT_PAREN: TOKEN_RIGHT_PAREN,
TOKEN_LEFT_BRACKET: TOKEN_LEFT_BRACKET,
TOKEN_RIGHT_BRACKET: TOKEN_RIGHT_BRACKET,
TOKEN_LEFT_CURLY: TOKEN_LEFT_CURLY,
TOKEN_RIGHT_CURLY: TOKEN_RIGHT_CURLY,
TOKEN_OPERATOR: TOKEN_OPERATOR,
TOKEN_COMMA: TOKEN_COMMA,
TOKEN_COLON: TOKEN_COLON,
TOKEN_PIPE: TOKEN_PIPE,
TOKEN_INT: TOKEN_INT,
TOKEN_FLOAT: TOKEN_FLOAT,
TOKEN_BOOLEAN: TOKEN_BOOLEAN,
TOKEN_SYMBOL: TOKEN_SYMBOL,
TOKEN_SPECIAL: TOKEN_SPECIAL
};
})();
(function() {
var lexer = modules["lexer"];
var nodes = modules["nodes"];
var Object = modules["object"];
var lib = modules["lib"];
var Parser = Object.extend({
init: function (tokens) {
this.tokens = tokens;
this.peeked = null;
this.breakOnBlocks = null;
this.dropLeadingWhitespace = false;
this.extensions = [];
},
nextToken: function (withWhitespace) {
var tok;
if(this.peeked) {
if(!withWhitespace && this.peeked.type == lexer.TOKEN_WHITESPACE) {
this.peeked = null;
}
else {
tok = this.peeked;
this.peeked = null;
return tok;
}
}
tok = this.tokens.nextToken();
if(!withWhitespace) {
while(tok && tok.type == lexer.TOKEN_WHITESPACE) {
tok = this.tokens.nextToken();
}
}
return tok;
},
peekToken: function () {
this.peeked = this.peeked || this.nextToken();
return this.peeked;
},
pushToken: function(tok) {
if(this.peeked) {
throw new Error("pushToken: can only push one token on between reads");
}
this.peeked = tok;
},
fail: function (msg, lineno, colno) {
if((lineno === undefined || colno === undefined) && this.peekToken()) {
var tok = this.peekToken();
lineno = tok.lineno;
colno = tok.colno;
}
if (lineno !== undefined) lineno += 1;
if (colno !== undefined) colno += 1;
throw new lib.TemplateError(msg, lineno, colno);
},
skip: function(type) {
var tok = this.nextToken();
if(!tok || tok.type != type) {
this.pushToken(tok);
return false;
}
return true;
},
expect: function(type) {
var tok = this.nextToken();
if(!tok.type == type) {
this.fail('expected ' + type + ', got ' + tok.type,
tok.lineno,
tok.colno);
}
return tok;
},
skipValue: function(type, val) {
var tok = this.nextToken();
if(!tok || tok.type != type || tok.value != val) {
this.pushToken(tok);
return false;
}
return true;
},
skipWhitespace: function () {
return this.skip(lexer.TOKEN_WHITESPACE);
},
skipSymbol: function(val) {
return this.skipValue(lexer.TOKEN_SYMBOL, val);
},
advanceAfterBlockEnd: function(name) {
if(!name) {
var tok = this.peekToken();
if(!tok) {
this.fail('unexpected end of file');
}
if(tok.type != lexer.TOKEN_SYMBOL) {
this.fail("advanceAfterBlockEnd: expected symbol token or " +
"explicit name to be passed");
}
name = this.nextToken().value;
}
var tok = this.nextToken();
if(tok && tok.type == lexer.TOKEN_BLOCK_END) {
if(tok.value.charAt(0) === '-') {
this.dropLeadingWhitespace = true;
}
}
else {
this.fail("expected block end in " + name + " statement");
}
},
advanceAfterVariableEnd: function() {
if(!this.skip(lexer.TOKEN_VARIABLE_END)) {
this.fail("expected variable end");
}
},
parseFor: function() {
var forTok = this.peekToken();
var node;
var endBlock;
if(this.skipSymbol('for')) {
node = new nodes.For(forTok.lineno, forTok.colno);
endBlock = 'endfor';
}
else if(this.skipSymbol('asyncEach')) {
node = new nodes.AsyncEach(forTok.lineno, forTok.colno);
endBlock = 'endeach';
}
else if(this.skipSymbol('asyncAll')) {
node = new nodes.AsyncAll(forTok.lineno, forTok.colno);
endBlock = 'endall';
}
else {
this.fail("parseFor: expected for{Async}", forTok.lineno, forTok.colno);
}
node.name = this.parsePrimary();
if(!(node.name instanceof nodes.Symbol)) {
this.fail('parseFor: variable name expected for loop');
}
var type = this.peekToken().type;
if(type == lexer.TOKEN_COMMA) {
// key/value iteration
var key = node.name;
node.name = new nodes.Array(key.lineno, key.colno);
node.name.addChild(key);
while(this.skip(lexer.TOKEN_COMMA)) {
var prim = this.parsePrimary();
node.name.addChild(prim);
}
}
if(!this.skipSymbol('in')) {
this.fail('parseFor: expected "in" keyword for loop',
forTok.lineno,
forTok.colno);
}
node.arr = this.parseExpression();
this.advanceAfterBlockEnd(forTok.value);
node.body = this.parseUntilBlocks(endBlock);
this.advanceAfterBlockEnd();
return node;
},
parseMacro: function() {
var macroTok = this.peekToken();
if(!this.skipSymbol('macro')) {
this.fail("expected macro");
}
var name = this.parsePrimary(true);
var args = this.parseSignature();
var node = new nodes.Macro(macroTok.lineno,
macroTok.colno,
name,
args);
this.advanceAfterBlockEnd(macroTok.value);
node.body = this.parseUntilBlocks('endmacro');
this.advanceAfterBlockEnd();
return node;
},
parseImport: function() {
var importTok = this.peekToken();
if(!this.skipSymbol('import')) {
this.fail("parseImport: expected import",
importTok.lineno,
importTok.colno);
}
var template = this.parsePrimary();
if(!this.skipSymbol('as')) {
this.fail('parseImport: expected "as" keyword',
importTok.lineno,
importTok.colno);
}
var target = this.parsePrimary();
var node = new nodes.Import(importTok.lineno,
importTok.colno,
template,
target);
this.advanceAfterBlockEnd(importTok.value);
return node;
},
parseFrom: function() {
var fromTok = this.peekToken();
if(!this.skipSymbol('from')) {
this.fail("parseFrom: expected from");
}
var template = this.parsePrimary();
var node = new nodes.FromImport(fromTok.lineno,
fromTok.colno,
template,
new nodes.NodeList());
if(!this.skipSymbol('import')) {
this.fail("parseFrom: expected import",
fromTok.lineno,
fromTok.colno);
}
var names = node.names;
while(1) {
var nextTok = this.peekToken();
if(nextTok.type == lexer.TOKEN_BLOCK_END) {
if(!names.children.length) {
this.fail('parseFrom: Expected at least one import name',
fromTok.lineno,
fromTok.colno);
}
// Since we are manually advancing past the block end,
// need to keep track of whitespace control (normally
// this is done in `advanceAfterBlockEnd`
if(nextTok.value.charAt(0) == '-') {
this.dropLeadingWhitespace = true;
}
this.nextToken();
break;
}
if(names.children.length > 0 && !this.skip(lexer.TOKEN_COMMA)) {
this.fail('parseFrom: expected comma',
fromTok.lineno,
fromTok.colno);
}
var name = this.parsePrimary();
if(name.value.charAt(0) == '_') {
this.fail('parseFrom: names starting with an underscore ' +
'cannot be imported',
name.lineno,
name.colno);
}
if(this.skipSymbol('as')) {
var alias = this.parsePrimary();
names.addChild(new nodes.Pair(name.lineno,
name.colno,
name,
alias));
}
else {
names.addChild(name);
}
}
return node;
},
parseBlock: function() {
var tag = this.peekToken();
if(!this.skipSymbol('block')) {
this.fail('parseBlock: expected block', tag.lineno, tag.colno);
}
var node = new nodes.Block(tag.lineno, tag.colno);
node.name = this.parsePrimary();
if(!(node.name instanceof nodes.Symbol)) {
this.fail('parseBlock: variable name expected',
tag.lineno,
tag.colno);
}
this.advanceAfterBlockEnd(tag.value);
node.body = this.parseUntilBlocks('endblock');
if(!this.peekToken()) {
this.fail('parseBlock: expected endblock, got end of file');
}
this.advanceAfterBlockEnd();
return node;
},
parseTemplateRef: function(tagName, nodeType) {
var tag = this.peekToken();
if(!this.skipSymbol(tagName)) {
this.fail('parseTemplateRef: expected '+ tagName);
}
var node = new nodeType(tag.lineno, tag.colno);
node.template = this.parsePrimary();
this.advanceAfterBlockEnd(tag.value);
return node;
},
parseExtends: function() {
return this.parseTemplateRef('extends', nodes.Extends);
},
parseInclude: function() {
return this.parseTemplateRef('include', nodes.Include);
},
parseIf: function() {
var tag = this.peekToken();
var node;
if(this.skipSymbol('if') || this.skipSymbol('elif')) {
node = new nodes.If(tag.lineno, tag.colno);
}
else if(this.skipSymbol('ifAsync')) {
node = new nodes.IfAsync(tag.lineno, tag.colno);
}
else {
this.fail("parseIf: expected if or elif",
tag.lineno,
tag.colno);
}
node.cond = this.parseExpression();
this.advanceAfterBlockEnd(tag.value);
node.body = this.parseUntilBlocks('elif', 'else', 'endif');
var tok = this.peekToken();
switch(tok && tok.value) {
case "elif":
node.else_ = this.parseIf();
break;
case "else":
this.advanceAfterBlockEnd();
node.else_ = this.parseUntilBlocks("endif");
this.advanceAfterBlockEnd();
break;
case "endif":
node.else_ = null;
this.advanceAfterBlockEnd();
break;
default:
this.fail('parseIf: expected endif, else, or endif, ' +
'got end of file');
}
return node;
},
parseSet: function() {
var tag = this.peekToken();
if(!this.skipSymbol('set')) {
this.fail('parseSet: expected set', tag.lineno, tag.colno);
}
var node = new nodes.Set(tag.lineno, tag.colno, []);
var target;
while((target = this.parsePrimary())) {
node.targets.push(target);
if(!this.skip(lexer.TOKEN_COMMA)) {
break;
}
}
if(!this.skipValue(lexer.TOKEN_OPERATOR, '=')) {
this.fail('parseSet: expected = in set tag',
tag.lineno,
tag.colno);
}
node.value = this.parseExpression();
this.advanceAfterBlockEnd(tag.value);
return node;
},
parseStatement: function () {
var tok = this.peekToken();
var node;
if(tok.type != lexer.TOKEN_SYMBOL) {
this.fail('tag name expected', tok.lineno, tok.colno);
}
if(this.breakOnBlocks &&
this.breakOnBlocks.indexOf(tok.value) !== -1) {
return null;
}
switch(tok.value) {
case