Skip to content

Commit

Permalink
Counter: 19. Improved Error Handling - opt in based on $.views.debugM…
Browse files Browse the repository at this point in the history
…ode.

See #134
Also fix for #137
  • Loading branch information
BorisMoore committed Jun 29, 2012
1 parent e5a2477 commit 9d8c2f7
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 37 deletions.
80 changes: 49 additions & 31 deletions jsrender.js
Expand Up @@ -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) {

Expand Down Expand Up @@ -58,11 +58,17 @@ this.jsviews || this.jQuery && jQuery.views || (function(global, undefined) {
delimiters: setDelimiters,
_convert: convert,
_err: function(e) {
return jsv.debugMode ? ("<br/><b>Error:</b> <em> " + (e.message || e) + ". </em>") : '""';
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 ==========================

//===================
Expand Down Expand Up @@ -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];
}
Expand All @@ -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);
}

//=================
Expand All @@ -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 || {},
Expand All @@ -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;
}

Expand All @@ -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;
Expand Down Expand Up @@ -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 = "";

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 "";
}

//===========================
Expand All @@ -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) {
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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 ====
Expand All @@ -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);

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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, '"')
Expand Down Expand Up @@ -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 "";
}
Expand All @@ -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,
Expand Down
12 changes: 6 additions & 6 deletions test/unit/jsrender-tests-no-jquery.js
Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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}}" );
Expand Down Expand Up @@ -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");
Expand All @@ -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}}" );
Expand Down Expand Up @@ -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' );
});

0 comments on commit 9d8c2f7

Please sign in to comment.