From 9d8c2f7d8403d53e1ad202cfad564be47a5f2062 Mon Sep 17 00:00:00 2001 From: Boris Moore Date: Thu, 28 Jun 2012 21:11:36 -0700 Subject: [PATCH] Counter: 19. Improved Error Handling - opt in based on $.views.debugMode. See https://github.com/BorisMoore/jsrender/issues/134 Also fix for https://github.com/BorisMoore/jsrender/issues/137 --- jsrender.js | 80 ++++++++++++++++----------- test/unit/jsrender-tests-no-jquery.js | 12 ++-- 2 files changed, 55 insertions(+), 37 deletions(-) diff --git a/jsrender.js b/jsrender.js index 4bee065..e890dc7 100644 --- a/jsrender.js +++ b/jsrender.js @@ -6,7 +6,7 @@ * Copyright 2012, Boris Moore * Released under the MIT License. */ -// informal pre beta commit counter: 18 +// informal pre beta commit counter: 19 this.jsviews || this.jQuery && jQuery.views || (function(global, undefined) { @@ -58,11 +58,17 @@ this.jsviews || this.jQuery && jQuery.views || (function(global, undefined) { delimiters: setDelimiters, _convert: convert, _err: function(e) { - return jsv.debugMode ? ("
Error: " + (e.message || e) + ". ") : '""'; + return jsv.debugMode ? ("Error: " + (e.message || e)) + ". " : '""'; }, _tmplFn: tmplFn, - _tag: renderTag + _tag: renderTag, + Error: function(message) { // Error exception type for JsViews/JsRender + this.name = "JsRender Error", + this.message = message || "JsRender error" + } }; + + (jsv.Error.prototype = new Error()).constructor = jsv.Error; //========================== Top-level functions ========================== //=================== @@ -92,7 +98,8 @@ this.jsviews || this.jQuery && jQuery.views || (function(global, undefined) { // Default rTag: tag converter colon html code params slash closeBlock // /{{(?:(?:(\w+(?=[\/\s}]))|(?:(\w+)?(:)|(>)|(\*)))\s*((?:[^}]|}(?!}))*?)(\/)?|(?:\/(\w+)))}} - rTmplString = new RegExp("<.*>|" + openChars + ".*" + closeChars); + rTmplString = new RegExp("<.*>|([^\\\\]|^)[{}]|" + delimOpenChar0 + delimOpenChar1 + ".*" + delimCloseChar0 + delimCloseChar1); + // rTmplString looks for html tags or { or } char not preceeded by \\, or JsRender tags {{xxx}}. Each of these strings are considered NOT to be jQuery selectors } return [delimOpenChar0, delimOpenChar1, delimCloseChar0, delimCloseChar1]; } @@ -117,9 +124,9 @@ this.jsviews || this.jQuery && jQuery.views || (function(global, undefined) { //================= function convert(converter, view, tmpl, text) { - var tmplConverters = tmpl.converters; - converter = tmplConverters && tmplConverters[converter] || converters[converter]; - return converter ? converter.call(view, text) : text; + var tmplConverter = tmpl.converters; + tmplConverter = tmplConverter && tmplConverter[converter] || converters[converter]; + return tmplConverter ? tmplConverter.call(view, text) : (error("Unknown converter: {{"+ converter + ":"), text); } //================= @@ -129,7 +136,7 @@ this.jsviews || this.jQuery && jQuery.views || (function(global, undefined) { function renderTag(tag, parentView, parentTmpl, converter, content, tagObject) { // Called from within compiled template function, to render a nested tag // Returns the rendered tag - var ret, prop, + var ret, tmplTags = parentTmpl.tags, nestedTemplates = parentTmpl.templates, props = tagObject.props = tagObject.props || {}, @@ -138,10 +145,11 @@ this.jsviews || this.jQuery && jQuery.views || (function(global, undefined) { tagFn = tmplTags && tmplTags[tag] || tags[tag]; if (!tagFn) { + error("Unknown tag: {{"+ tag + "}}"); return ""; } if (tmpl) { - // We don't want to expose tmpl as a prop to view context + // We don't want to expose tmpl as a prop to view context in rendered content delete props.tmpl; } @@ -150,9 +158,9 @@ this.jsviews || this.jQuery && jQuery.views || (function(global, undefined) { tmpl = tmpl || content || undefined; tagObject.tmpl = - "" + tmpl === tmpl // if a string - ? nestedTemplates && nestedTemplates[tmpl] || templates[tmpl] || templates(tmpl) - : tmpl; + "" + tmpl === tmpl // if a string + ? nestedTemplates && nestedTemplates[tmpl] || templates[tmpl] || templates(tmpl) + : tmpl; tagObject.isTag = TRUE; tagObject.converter = converter; @@ -282,7 +290,7 @@ this.jsviews || this.jQuery && jQuery.views || (function(global, undefined) { function renderContent(data, context, parentView, key, isLayout, path, onRender) { // Render template against data as a tree of subviews (nested template), or as a string (top-level template). - var i, l, dataItem, newView, itemWrap, itemsWrap, itemResult, parentContext, tmpl, props, hasProp, swapContent, isLayout, mergedCtx, + var i, l, dataItem, newView, itemResult, parentContext, tmpl, props, hasProp, swapContent, isLayout, mergedCtx, self = this, result = ""; @@ -323,8 +331,8 @@ this.jsviews || this.jQuery && jQuery.views || (function(global, undefined) { path = path || self.path; key = key || self.key; } else { - tmpl = self.jquery && self[0] // This is a call from $(selector).render - || self; // This is a call from tmpl.render + tmpl = self.jquery && (self[0] || error('Unknown template: "' + self.selector + '"')) // This is a call from $(selector).render + || self; } if (tmpl) { if (parentView) { @@ -374,7 +382,8 @@ this.jsviews || this.jQuery && jQuery.views || (function(global, undefined) { return onRender ? onRender(result, tmpl, props, newView.key, path) : result; } } - return ""; // No tmpl. Could throw... + error("No template found"); + return ""; } //=========================== @@ -384,8 +393,14 @@ this.jsviews || this.jQuery && jQuery.views || (function(global, undefined) { // Generate a reusable function that will serve to render a template against data // (Compile AST then build template function) - function syntaxError(message, e) { - throw (e ? (e.name + ': "' + e.message + '"') : "Syntax error") + (message ? (" \n" + message) : ""); + function error(message) { + if (jsv.debugMode) { + throw new jsv.Error(message); + } + } + + function syntaxError(message) { + error("Syntax error\n" + message); } function tmplFn(markup, tmpl, bind) { @@ -412,6 +427,10 @@ this.jsviews || this.jQuery && jQuery.views || (function(global, undefined) { } } + function blockTagCheck(tagName) { + tagName && syntaxError('Unmatched or missing tag: "{{/' + tagName + '}}" in template:\n' + markup); + } + function parseTag(all, tagName, converter, colon, html, code, params, slash, closeBlock, index) { // tag converter colon html code params slash closeBlock // /{{(?:(?:(\w+(?=[\/!\s\}!]))|(?:(\w+)?(:)|(?:(>)|(\*)))((?:[^\}]|}(?!}))*?)(\/)?|(?:\/(\w+)))}}/g; @@ -420,7 +439,8 @@ this.jsviews || this.jQuery && jQuery.views || (function(global, undefined) { colon = ":"; converter = "html"; } - var hash = "", + var current0, + hash = "", passedCtx = "", // Block tag if not self-closing and not {{:}} or {{>}} (special case) and not a data-link expression (has bind parameter) block = !slash && !colon && !bind; @@ -468,15 +488,12 @@ this.jsviews || this.jQuery && jQuery.views || (function(global, undefined) { } content.push(newNode); } else if (closeBlock) { - //if ( closeBlock !== current[ 0 ]) { - // throw "unmatched close tag: /" + closeBlock + ". Expected /" + current[ 0 ]; - //} + current0 = current[0]; + blockTagCheck(closeBlock !== current0 && !(closeBlock === "if" && current0 === "else") && current0); current[5] = markup.substring(current[5], index); // contentMarkup for block tag current = stack.pop(); } - if (!current) { - throw "Expected block tag"; - } + blockTagCheck(!current && closeBlock); content = current[3]; } //==== /end of nested functions ==== @@ -485,6 +502,7 @@ this.jsviews || this.jQuery && jQuery.views || (function(global, undefined) { // Build the AST (abstract syntax tree) under astTop markup.replace(rTag, parseTag); + blockTagCheck(stack[0] && stack[0][3].pop()[0]); pushPreceedingContent(markup.length); @@ -542,7 +560,7 @@ this.jsviews || this.jQuery && jQuery.views || (function(global, undefined) { try { code = new Function("data, view, j, b, u", code); } catch (e) { - syntaxError("Error in compiled template code:\n" + code, e); + syntaxError("Compiled template code:\n\n" + code, e); } // Include only the var references that are needed in the code @@ -605,7 +623,7 @@ this.jsviews || this.jQuery && jQuery.views || (function(global, undefined) { } if (err) { - syntaxError(); + syntaxError(params); } else { return (aposed // within single-quoted string @@ -627,7 +645,7 @@ this.jsviews || this.jQuery && jQuery.views || (function(global, undefined) { ) : eq // named param - ? (parenDepth && syntaxError(), named = TRUE, '\b' + path + ':') + ? (parenDepth && syntaxError(params), named = TRUE, '\b' + path + ':') : path // path ? (path.replace(rPath, parsePath) @@ -645,7 +663,7 @@ this.jsviews || this.jQuery && jQuery.views || (function(global, undefined) { : "") ) : comma - ? (fnCall[parenDepth] || syntaxError(), ",") // We don't allow top-level literal arrays or objects + ? (fnCall[parenDepth] || syntaxError(params), ",") // We don't allow top-level literal arrays or objects : lftPrn0 ? "" : (aposed = apos, quoted = quot, '"') @@ -839,7 +857,7 @@ this.jsviews || this.jQuery && jQuery.views || (function(global, undefined) { l = args.length; while (l && !args[i++]) { - // Only render content if args.length === 0 (i.e. this is an else with no condition) or if a condition argument is truey + // Only render content if args.length === 0 (i.e. this is an else) or if a condition argument is truey if (i === l) { return ""; } @@ -855,7 +873,7 @@ this.jsviews || this.jQuery && jQuery.views || (function(global, undefined) { }, "else": function() { var view = this.view; - return view.onElse ? view.onElse(this, arguments) : ""; + return view.onElse ? view.onElse( this, arguments ) : ""; }, "for": function() { var i, diff --git a/test/unit/jsrender-tests-no-jquery.js b/test/unit/jsrender-tests-no-jquery.js index c88c5d1..4a85e0b 100644 --- a/test/unit/jsrender-tests-no-jquery.js +++ b/test/unit/jsrender-tests-no-jquery.js @@ -4,7 +4,7 @@ function compileTmpl( template ) { return typeof jsviews.templates( template ).fn === "function" ? "compiled" : "failed compile"; } catch(e) { - return "error:" + e; + return e.message; } } @@ -38,7 +38,7 @@ test("{{if}} {{else}}", function() { expect(3); equal( compileTmpl( "A_{{if true}}{{/if}}_B" ), "compiled", "Empty if block: {{if}}{{/if}}" ); equal( compileTmpl( "A_{{if true}}yes{{/if}}_B" ), "compiled", "{{if}}...{{/if}}" ); - equal( compileTmpl( "A_{{if true/}}yes{{/if}}_B" ), "error:Expected block tag", "Mismatching block tags: {{if/}} {{/if}}" ); + equal( compileTmpl( "A_{{if true/}}yes{{/if}}_B" ), "Syntax error\nUnmatched or missing tag: \"{{/if}}\" in template:\nA_{{if true/}}yes{{/if}}_B"); }); module( "{{if}}" ); @@ -170,8 +170,8 @@ test("values", function() { test("expressions", function() { expect(8); - equal( compileTmpl( "{{:a++}}" ), "error:Syntax error", "a++" ); - equal( compileTmpl( "{{:(a,b)}}" ), "error:Syntax error", "(a,b)" ); + equal( compileTmpl( "{{:a++}}" ), "Syntax error\na++", "a++" ); + equal( compileTmpl( "{{:(a,b)}}" ), "Syntax error\n(a,b)", "(a,b)" ); equal( jsviews.templates( "{{: a+2}}" ).render({ a: 2, b: false }), "4", "a+2"); equal( jsviews.templates( "{{: b?'yes':'no' }}" ).render({ a: 2, b: false }), "no", "b?'yes':'no'"); equal( jsviews.templates( "{{:(a||-1) + (b||-1) }}" ).render({ a: 2, b: 0 }), "1", "a||-1"); @@ -195,7 +195,7 @@ test("{{for}}", function() { equal( jsviews.render.templateForArray( [people] ), "header_JoBill_footer", 'Can render a template against an array, as a "layout template", by wrapping array in an array' ); equal( jsviews.render.pageTmpl({ people: people }), "header_JoBill_footer", '{{for [people] tmpl="templateForArray"/}}' ); equal( jsviews.templates( "{{for people towns}}{{:name}}{{/for}}" ).render({ people: people, towns: towns }), "JoBillSeattleParisDelhi", "concatenated targets: {{for people towns}}" ); - equal( jsviews.templates( "{{for}}xxx{{:#data===~undefined}}{{/for}}" ).render(), "xxxtrue", "no parameter - renders once with #data undefined: {{for}}" ); + equal( jsviews.templates( "{{for}}xxx{{:#data===~test}}{{/for}}" ).render({},{test:undefined}), "xxxtrue", "no parameter - renders once with #data undefined: {{for}}" ); equal( jsviews.templates( "{{for missingProperty}}xxx{{:#data===~undefined}}{{/for}}" ).render({}), "xxxtrue", "missingProperty - renders once with #data undefined: {{for missingProperty}}" ); equal( jsviews.templates( "{{for null}}xxx{{:#data===null}}{{/for}}" ).render(), "xxxtrue", "null - renders once with #data null: {{for null}}" ); equal( jsviews.templates( "{{for false}}xxx{{:#data}}{{/for}}" ).render(), "xxxfalse", "false - renders once with #data false: {{for false}}" ); @@ -456,7 +456,7 @@ test("template encapsulation", function() { } }; equal( jsviews.render.tmplWithNestedResources({ a: "aVal" }), "aval aValbtrue% (double:aVal&aVal) (override outer double:AVAL|AVAL)", 'Access nested resources from template' ); - equal( jsviews.render.useLower({ a: "aVal" }), "", 'Cannot access nested resources from a different template' ); + equal( jsviews.render.useLower({ a: "aVal" }), "Error: Unknown tag: {{lower}}. ", 'Cannot access nested resources from a different template' ); equal( jsviews.render.tmplWithNestedResources({ a: "aVal" }, context), "aval aValbcontextualNot2true% (double:aVal&aVal) (override outer double:contextualUpperAVAL|contextualUpperAVAL)", 'Resources passed in with context override nested resources' ); equal( jsviews.templates.tmplWithNestedResources.templates.templateWithDebug.fn.toString().indexOf("debugger;") > 0, true, 'Can set debug=true on nested template' ); });