diff --git a/browser/nunjucks-dev.js b/browser/nunjucks-dev.js index a0548a9f..d35bc832 100644 --- a/browser/nunjucks-dev.js +++ b/browser/nunjucks-dev.js @@ -65,6 +65,45 @@ var ObjProto = Object.prototype; var exports = modules['lib'] = {}; +exports.TemplateError = function(message, lineno, colno) { + var self = this; + + if (message instanceof Error) { // for casting regular js errors + self = message; + message = message.name + ": " + message.message; + } else { + Error.captureStackTrace(self); + } + + self.name = "Template render error"; + self.message = message; + self.lineno = lineno; + self.colno = colno; + self.firstUpdate = true; + + self.Update = function(path) { + var message = "(" + (path || "unknown path") + ")"; + + // only show lineno + colno next to path of template + // where error occurred + if (this.firstUpdate && this.lineno && this.colno) { + message += ' [Line ' + this.lineno + ', Column ' + this.colno + ']'; + } + + message += '\n '; + if (this.firstUpdate) { + message += ' '; + } + + this.message = message + (this.message || ''); + this.firstUpdate = false; + return this; + }; + return self; +}; +exports.TemplateError.prototype = Error.prototype; + + exports.isFunction = function(obj) { return ObjProto.toString.call(obj) == '[object Function]'; }; @@ -441,7 +480,10 @@ var Frame = Object.extend({ lookup: function(name) { var p = this.parent; - return this.variables[name] || (p && p.lookup(name)); + var val = this.variables[name]; + return (val !== undefined && val !== null) ? + val : + (p && p.lookup(name)); }, push: function() { @@ -520,11 +562,39 @@ function numArgs(args) { } } +function suppressValue(val) { + return (val !== undefined && val !== null) ? val : ""; +} + +function suppressLookupValue(obj, val) { + obj = obj || {}; + val = obj[val]; + + if(typeof val === 'function') { + return function() { + return suppressValue(val.apply(obj, arguments)); + }; + } + else { + return suppressValue(val); + } +} + +function contextOrFrameLookup(context, frame, name) { + var val = context.lookup(name); + return (val !== undefined && val !== null) ? + val : + frame.lookup(name); +} + modules['runtime'] = { Frame: Frame, makeMacro: makeMacro, makeKeywordArgs: makeKeywordArgs, - numArgs: numArgs + numArgs: numArgs, + suppressValue: suppressValue, + suppressLookupValue: suppressLookupValue, + contextOrFrameLookup: contextOrFrameLookup }; })(); (function() { @@ -992,17 +1062,15 @@ var Parser = Object.extend({ }, fail: function (msg, lineno, colno) { - if((!lineno || !colno) && this.peekToken()) { + 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; - if(lineno && colno) { - msg = '[Line ' + (lineno + 1) + ', Column ' + (colno + 1) + '] ' + msg; - } - - throw new Error(msg); + throw new lib.TemplateError(msg, lineno, colno); }, skip: function(type) { @@ -1071,7 +1139,7 @@ var Parser = Object.extend({ parseFor: function() { var forTok = this.peekToken(); if(!this.skipSymbol('for')) { - this.fail("expected for"); + this.fail("parseFor: expected for", forTok.lineno, forTok.colno); } var node = new nodes.For(forTok.lineno, forTok.colno); @@ -1079,7 +1147,7 @@ var Parser = Object.extend({ node.name = this.parsePrimary(); if(!(node.name instanceof nodes.Symbol)) { - this.fail('variable name expected'); + this.fail('parseFor: variable name expected for loop'); } if(this.skip(lexer.TOKEN_COMMA)) { @@ -1091,7 +1159,9 @@ var Parser = Object.extend({ } if(!this.skipSymbol('in')) { - this.fail('expected "in" keyword'); + this.fail('parseFor: expected "in" keyword for loop', + forTok.lineno, + forTok.colno); } node.arr = this.parseExpression(); @@ -1126,13 +1196,17 @@ var Parser = Object.extend({ parseImport: function() { var importTok = this.peekToken(); if(!this.skipSymbol('import')) { - this.fail("expected import"); + this.fail("parseImport: expected import", + importTok.lineno, + importTok.colno); } var template = this.parsePrimary(); if(!this.skipSymbol('as')) { - throw new Error('expected "as" keyword'); + throw new Error('parseImport: expected "as" keyword', + importTok.lineno, + importTok.colno); } var target = this.parsePrimary(); @@ -1148,7 +1222,7 @@ var Parser = Object.extend({ parseFrom: function() { var fromTok = this.peekToken(); if(!this.skipSymbol('from')) { - this.fail("expected from"); + this.fail("parseFrom: expected from"); } var template = this.parsePrimary(); @@ -1158,7 +1232,9 @@ var Parser = Object.extend({ new nodes.NodeList()); if(!this.skipSymbol('import')) { - throw new Error("expected import"); + throw new Error("parseFrom: expected import", + fromTok.lineno, + fromTok.colno); } var names = node.names; @@ -1167,7 +1243,9 @@ var Parser = Object.extend({ var type = this.peekToken().type; if(type == lexer.TOKEN_BLOCK_END) { if(!names.children.length) { - this.fail('Expected at least one import name'); + this.fail('parseFrom: Expected at least one import name', + fromTok.lineno, + fromTok.colno); } this.nextToken(); @@ -1175,13 +1253,15 @@ var Parser = Object.extend({ } if(names.children.length > 0 && !this.skip(lexer.TOKEN_COMMA)) { - throw new Error('expected comma'); + throw new Error('parseFrom: expected comma', + fromTok.lineno, + fromTok.colno); } var name = this.parsePrimary(); if(name.value.charAt(0) == '_') { - this.fail('names starting with an underscore cannot be ' + - 'imported', + this.fail('parseFrom: names starting with an underscore ' + + 'cannot be imported', name.lineno, name.colno); } @@ -1204,14 +1284,16 @@ var Parser = Object.extend({ parseBlock: function() { var tag = this.peekToken(); if(!this.skipSymbol('block')) { - this.fail('expected 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('variable name expected'); + this.fail('parseBlock: variable name expected', + tag.lineno, + tag.colno); } this.advanceAfterBlockEnd(tag.value); @@ -1219,7 +1301,7 @@ var Parser = Object.extend({ node.body = this.parseUntilBlocks('endblock'); if(!this.peekToken()) { - this.fail('expected endblock, got end of file'); + this.fail('parseBlock: expected endblock, got end of file'); } this.advanceAfterBlockEnd(); @@ -1230,17 +1312,11 @@ var Parser = Object.extend({ parseTemplateRef: function(tagName, nodeType) { var tag = this.peekToken(); if(!this.skipSymbol(tagName)) { - this.fail('expected '+ tagName); + this.fail('parseTemplateRef: expected '+ tagName); } var node = new nodeType(tag.lineno, tag.colno); - node.template = this.parsePrimary(); - if(!(node.template instanceof nodes.Literal && - lib.isString(node.template.value)) && - !(node.template instanceof nodes.Symbol)) { - this.fail('parseExtends: string or value expected'); - } this.advanceAfterBlockEnd(tag.value); return node; @@ -1257,7 +1333,9 @@ var Parser = Object.extend({ parseIf: function() { var tag = this.peekToken(); if(!this.skipSymbol('if') && !this.skipSymbol('elif')) { - this.fail("expected if or elif"); + this.fail("parseIf: expected if or elif", + tag.lineno, + tag.colno); } var node = new nodes.If(tag.lineno, tag.colno); @@ -1282,7 +1360,8 @@ var Parser = Object.extend({ this.advanceAfterBlockEnd(); break; default: - this.fail('expected endif, else, or endif, got end of file'); + this.fail('parseIf: expected endif, else, or endif, ' + + 'got end of file'); } return node; @@ -1291,7 +1370,7 @@ var Parser = Object.extend({ parseSet: function() { var tag = this.peekToken(); if(!this.skipSymbol('set')) { - this.fail('expected set'); + this.fail('parseSet: expected set', tag.lineno, tag.colno); } var node = new nodes.Set(tag.lineno, tag.colno, []); @@ -1306,7 +1385,9 @@ var Parser = Object.extend({ } if(!this.skipValue(lexer.TOKEN_OPERATOR, '=')) { - this.fail('expected = in set tag'); + this.fail('parseSet: expected = in set tag', + tag.lineno, + tag.colno); } node.value = this.parseExpression(); @@ -1339,7 +1420,7 @@ var Parser = Object.extend({ case 'macro': node = this.parseMacro(); break; case 'import': node = this.parseImport(); break; case 'from': node = this.parseFrom(); break; - default: this.fail('unknown block tag: ' + tok.value); + default: this.fail('unknown block tag: ' + tok.value, tok.lineno, tok.colno); } return node; @@ -1389,7 +1470,14 @@ var Parser = Object.extend({ } } - return new nodes.TemplateData(begun.lineno, begun.colno, str); + + var output = new nodes.Output( + begun.lineno, + begun.colno, + [new nodes.TemplateData(begun.lineno, begun.colno, str)] + ); + + return output; }, parsePostfix: function(node) { @@ -1421,7 +1509,9 @@ var Parser = Object.extend({ var val = this.nextToken(); if(val.type != lexer.TOKEN_SYMBOL) { - this.fail('expected name as lookup value, got ' + val.value); + this.fail('expected name as lookup value, got ' + val.value, + val.lineno, + val.colno); } // Make a literal string because it's not a variable @@ -1893,7 +1983,7 @@ var util = modules["util"]; // console.log(util.inspect(t)); // } -// var p = new Parser(lexer.lex('{{ foo(1, 2, 3, foo=3) }}')); +// var p = new Parser(lexer.lex('{% raw %}hello{% endraw %}')); // var n = p.parse(); // nodes.printNodes(n); @@ -1934,7 +2024,7 @@ function binOpEmitter(str) { // Generate an array of strings function quotedArray(arr) { - return '[' + + return '[' + lib.map(arr, function(x) { return '"' + x + '"'; }) + ']'; } @@ -1946,6 +2036,12 @@ var Compiler = Object.extend({ this.buffer = null; this.isChild = false; }, + fail: function (msg, lineno, colno) { + if (lineno !== undefined) lineno += 1; + if (colno !== undefined) colno += 1; + + throw new lib.TemplateError(msg, lineno, colno); + }, emit: function(code) { this.codebuf.push(code); @@ -2002,19 +2098,33 @@ var Compiler = Object.extend({ }, _compileExpression: function(node, frame) { - this.assertType(node, - nodes.Literal, - nodes.Symbol, - nodes.Group, - nodes.Array, - nodes.Dict, - nodes.FunCall, - nodes.Filter, - nodes.LookupVal, - nodes.Compare, - nodes.And, - nodes.Or, - nodes.Not); + // TODO: I'm not really sure if this type check is worth it or + // not. + this.assertType( + node, + nodes.Literal, + nodes.Symbol, + nodes.Group, + nodes.Array, + nodes.Dict, + nodes.FunCall, + nodes.Filter, + nodes.LookupVal, + nodes.Compare, + nodes.And, + nodes.Or, + nodes.Not, + nodes.Add, + nodes.Sub, + nodes.Mul, + nodes.Div, + nodes.FloorDiv, + nodes.Mod, + nodes.Pow, + nodes.Neg, + nodes.Pos, + nodes.Compare + ); this.compile(node, frame); }, @@ -2026,10 +2136,12 @@ var Compiler = Object.extend({ if(node instanceof types[i]) { success = true; } - }; + } if(!success) { - throw new Error("invalid type: " + node.typename); + this.fail("assertType: invalid type: " + node.typename, + node.lineno, + node.colno); } }, @@ -2039,7 +2151,8 @@ var Compiler = Object.extend({ compileLiteral: function(node, frame) { if(typeof node.value == "string") { - var val = node.value.replace(/"/g, '\\"'); + var val = node.value.replace(/\\/g, '\\\\'); + val = val.replace(/"/g, '\\"'); val = val.replace(/\n/g, "\\n"); val = val.replace(/\r/g, "\\r"); val = val.replace(/\t/g, "\\t"); @@ -2058,9 +2171,9 @@ var Compiler = Object.extend({ this.emit(v); } else { - this.emit('context.lookup("' + name + '") || ' + - 'frame.lookup("' + name + '") || ' + - '""'); + this.emit('runtime.suppressValue(' + + 'runtime.contextOrFrameLookup(' + + 'context, frame, "' + name + '"))'); } }, @@ -2085,7 +2198,9 @@ var Compiler = Object.extend({ } else if(!(key instanceof nodes.Literal && typeof key.value == "string")) { - throw new Error("Dict keys must be strings or names"); + this.fail("compilePair: Dict keys must be strings or names", + key.lineno, + key.colno); } this.compile(key, frame); @@ -2143,12 +2258,11 @@ var Compiler = Object.extend({ }, compileLookupVal: function(node, frame) { - this.emit('('); + this.emit('runtime.suppressLookupValue(('); this._compileExpression(node.target, frame); - this.emit(')'); - this.emit('['); + this.emit('),'); this._compileExpression(node.val, frame); - this.emit(']'); + this.emit(')'); }, compileFunCall: function(node, frame) { @@ -2182,7 +2296,7 @@ var Compiler = Object.extend({ var id = this.tmpid(); this.emit('var ' + id + ' = '); - this._compileExpression(node.value); + this._compileExpression(node.value, frame); this.emitLine(';'); for(var i=0; i2.5.11 + app.render = function(name, ctx, k) { + var context = {}; - if(lib.isFunction(ctx)) { - k = ctx; - ctx = {}; - } + if(lib.isFunction(ctx)) { + k = ctx; + ctx = {}; + } - context = lib.extend(context, app.locals); + context = lib.extend(context, this.locals); - if(ctx._locals) { - context = lib.extend(context, ctx._locals); - } + if(ctx._locals) { + context = lib.extend(context, ctx._locals); + } - context = lib.extend(context, ctx); + context = lib.extend(context, ctx); - var res = env.render(name, context); - k(null, res); - }; + var res = env.render(name, context); + k(null, res); + }; + } + else { + // Express <2.5.11 + var http = modules["http"]; + var res = http.ServerResponse.prototype; + + res._render = function(name, ctx, k) { + var app = this.app; + var context = {}; + + if(this._locals) { + context = lib.extend(context, this._locals); + } + + if(ctx) { + context = lib.extend(context, ctx); + + if(ctx.locals) { + context = lib.extend(context, ctx.locals); + } + } + + context = lib.extend(context, app._locals); + var str = env.render(name, context); + + if(k) { + k(null, str); + } + else { + this.send(str); + } + }; + } }, render: function(name, ctx) { @@ -3092,7 +3280,9 @@ var Template = Object.extend({ this.upToDate = upToDate || function() { return false; }; if(eagerCompile) { - this._compile(); + var self = this; + this.env.tryTemplate(this.path, function() { self._compile(); }); + self = null; } else { this.compiled = false; @@ -3100,15 +3290,21 @@ var Template = Object.extend({ }, render: function(ctx, frame) { - if(!this.compiled) { - this._compile(); - } + var self = this; + + var render = function() { + if(!self.compiled) { + self._compile(); + } - var context = new Context(ctx || {}, this.blocks); - return this.rootRenderFunc(this.env, - context, - frame || new Frame(), - runtime); + var context = new Context(ctx || {}, self.blocks); + + return self.rootRenderFunc(self.env, + context, + frame || new Frame(), + runtime); + }; + return this.env.tryTemplate(this.path, render); }, isUpToDate: function() { diff --git a/browser/nunjucks-min.js b/browser/nunjucks-min.js index c3684f8b..885fe94a 100644 --- a/browser/nunjucks-min.js +++ b/browser/nunjucks-min.js @@ -1 +1 @@ -(function(){var a={};(function(){function b(a,c,d){var e=Object.create(a.prototype),f=/xyz/.test(function(){xyz})?/\bparent\b/:/.*/;d=d||{};for(var g in d){var h=d[g],i=e[g];typeof i=="function"&&typeof h=="function"&&f.test(h)?e[g]=function(a,b){return function(){var c=this.parent;this.parent=b;var d=a.apply(this,arguments);return this.parent=c,d}}(h,i):e[g]=h}e.typename=c;var j=function(){e.init&&e.init.apply(this,arguments)};return j.prototype=e,j.prototype.constructor=j,j.extend=function(a,c){return typeof a=="object"&&(c=a,a="anonymous"),b(j,a,c)},j}a.object=b(Object,"Object",{})})(),function(){var b=Array.prototype,c=Object.prototype,d=a.lib={};d.isFunction=function(a){return c.toString.call(a)=="[object Function]"},d.isArray=Array.isArray||function(a){return c.toString.call(a)=="[object Array]"},d.isString=function(a){return c.toString.call(a)=="[object String]"},d.isObject=function(a){return a===Object(a)},d.groupBy=function(a,b){var c={},e=d.isFunction(b)?b:function(a){return a[b]};for(var f=0;f=c)return a;var d=c-a.length,e=b.repeat(" ",d/2-d%2),f=b.repeat(" ",d/2);return e+a+f},"default":function(a,b){return a?a:b},escape:function(a){return a.replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")},first:function(a){return a[0]},groupby:function(a,c){return b.groupBy(a,c)},indent:function(a,c,d){c=c||4;var e="",f=a.split("\n"),g=b.repeat(" ",c);for(var h=0;h=d)break;f=e,e=e.replace(b,c),g++}return e},reverse:function(a){var d;return b.isString(a)?d=c.list(a):d=b.map(a,function(a){return a}),d.reverse(),b.isString(a)?d.join(""):d},round:function(a,b,c){b=b||0;var d=Math.pow(10,b),e;return c=="ceil"?e=Math.ceil:c=="floor"?e=Math.floor:e=Math.round,e(a*d)/d},slice:function(a,b,c){var d=Math.floor(a.length/b),e=a.length%b,f=0,g=[];for(var h=0;h=e&&k.push(c),g.push(k)}return g},sort:function(a,c,d,e){return a=b.map(a,function(a){return a}),a.sort(function(a,f){var g,h;return e?(g=a[e],h=f[e]):(g=a,h=f),!d&&b.isString(g)&&b.isString(h)&&(g=g.toLowerCase(),h=h.toLowerCase()),gh?c?-1:1:0}),a},string:function(a){return a.toString()},title:function(a){return a.toUpperCase()},trim:function(a){return a.replace(/^\s*|\s*$/g,"")},upper:function(a){return a.toUpperCase()},wordcount:function(a){return a.match(/\w+/g).length},"float":function(a,b){return parseFloat(a)||b},"int":function(a,b){return parseInt(a)||b}};c.d=c.default,c.e=c.escape,a.filters=c}(),function(){function d(a,b,c){return function(){var d=g(arguments),e,h=f(arguments);if(d>a.length){e=Array.prototype.slice.call(arguments,0,a.length);var i=Array.prototype.slice.call(arguments,e.length,d);for(var j=0;j=n)return e;var r=n-e.length,i=t.repeat(" ",r/2-r%2),s=t.repeat(" ",r/2);return i+e+s},"default":function(e,t){return e?e:t},escape:function(e){return e.replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")},first:function(e){return e[0]},groupby:function(e,n){return t.groupBy(e,n)},indent:function(e,n,r){n=n||4;var i="",s=e.split("\n"),o=t.repeat(" ",n);for(var u=0;u=r)break;s=i,i=i.replace(t,n),o++}return i},reverse:function(e){var r;return t.isString(e)?r=n.list(e):r=t.map(e,function(e){return e}),r.reverse(),t.isString(e)?r.join(""):r},round:function(e,t,n){t=t||0;var r=Math.pow(10,t),i;return n=="ceil"?i=Math.ceil:n=="floor"?i=Math.floor:i=Math.round,i(e*r)/r},slice:function(e,t,n){var r=Math.floor(e.length/t),i=e.length%t,s=0,o=[];for(var u=0;u=i&&l.push(n),o.push(l)}return o},sort:function(e,n,r,i){return e=t.map(e,function(e){return e}),e.sort(function(e,s){var o,u;return i?(o=e[i],u=s[i]):(o=e,u=s),!r&&t.isString(o)&&t.isString(u)&&(o=o.toLowerCase(),u=u.toLowerCase()),ou?n?-1:1:0}),e},string:function(e){return e.toString()},title:function(e){return e.toUpperCase()},trim:function(e){return e.replace(/^\s*|\s*$/g,"")},upper:function(e){return e.toUpperCase()},wordcount:function(e){return e.match(/\w+/g).length},"float":function(e,t){return parseFloat(e)||t},"int":function(e,t){return parseInt(e)||t}};n.d=n.default,n.e=n.escape,e.filters=n}(),function(){function r(e,t,n){return function(){var r=o(arguments),i,u=s(arguments);if(r>e.length){i=Array.prototype.slice.call(arguments,0,e.length);var a=Array.prototype.slice.call(arguments,i.length,r);for(var f=0;f2.5.11 + app.render = function(name, ctx, k) { + var context = {}; - if(lib.isFunction(ctx)) { - k = ctx; - ctx = {}; - } + if(lib.isFunction(ctx)) { + k = ctx; + ctx = {}; + } - context = lib.extend(context, app.locals); + context = lib.extend(context, this.locals); - if(ctx._locals) { - context = lib.extend(context, ctx._locals); - } + if(ctx._locals) { + context = lib.extend(context, ctx._locals); + } - context = lib.extend(context, ctx); + context = lib.extend(context, ctx); - var res = env.render(name, context); - k(null, res); - }; + var res = env.render(name, context); + k(null, res); + }; + } + else { + // Express <2.5.11 + var http = modules["http"]; + var res = http.ServerResponse.prototype; + + res._render = function(name, ctx, k) { + var app = this.app; + var context = {}; + + if(this._locals) { + context = lib.extend(context, this._locals); + } + + if(ctx) { + context = lib.extend(context, ctx); + + if(ctx.locals) { + context = lib.extend(context, ctx.locals); + } + } + + context = lib.extend(context, app._locals); + var str = env.render(name, context); + + if(k) { + k(null, str); + } + else { + this.send(str); + } + }; + } }, render: function(name, ctx) { @@ -774,7 +914,9 @@ var Template = Object.extend({ this.upToDate = upToDate || function() { return false; }; if(eagerCompile) { - this._compile(); + var self = this; + this.env.tryTemplate(this.path, function() { self._compile(); }); + self = null; } else { this.compiled = false; @@ -782,15 +924,21 @@ var Template = Object.extend({ }, render: function(ctx, frame) { - if(!this.compiled) { - this._compile(); - } + var self = this; - var context = new Context(ctx || {}, this.blocks); - return this.rootRenderFunc(this.env, - context, - frame || new Frame(), - runtime); + var render = function() { + if(!self.compiled) { + self._compile(); + } + + var context = new Context(ctx || {}, self.blocks); + + return self.rootRenderFunc(self.env, + context, + frame || new Frame(), + runtime); + }; + return this.env.tryTemplate(this.path, render); }, isUpToDate: function() {