diff --git a/mustache.js b/mustache.js index ea88e98..acdfd08 100644 --- a/mustache.js +++ b/mustache.js @@ -134,8 +134,24 @@ var Mustache = (function(undefined) { return Object.prototype.toString.call(a) === '[object Array]'; } - function create_error(line, character, message) { - return new Error('(' + line + ',' + character + '): ' + message); + function create_error(metrics, message) { + var str = '', err; + + if (metrics) { + str = '(' + metrics.line + ',' + metrics.character + '): '; + if (metrics.partial) { + str = '[' + metrics.partial + ']' + str; + } + } + + err = new Error(str + message); + if (metrics) { + err.line = metrics.line; + err.character = metrics.character; + err.partial = metrics.partial; + } + + return err; } /* END Helpers */ @@ -145,7 +161,7 @@ var Mustache = (function(undefined) { function compile(state, noReturn) { var n, c, token; - for (n = state.tokens.length;state.cursor]?\s*[^!{=]\S*?\s*}})|({{{\s*\S*?\s*}}})|({{=\S*?\s*\S*?=}})/; - function create_compiler_state(template, partials, openTag, closeTag) { + function create_compiler_state(template, partials, openTag, closeTag, parse_pragma) { openTag = openTag || '{{'; closeTag = closeTag || '}}'; @@ -208,9 +228,12 @@ var Mustache = (function(undefined) { tokenizer = new RegExp(parts.join('|')); } - var code = []; - var state = { - line: 1, character: 1 + var code = [], state = { + metrics: { + partial: null + , line: 1 + , character: 1 + } , template: template || '' , partials: partials || {} , openTag: openTag @@ -222,8 +245,10 @@ var Mustache = (function(undefined) { code.push(f); } }; - - pragmas(state); // use pragmas to determine parsing behaviour + + if (parse_pragma!==false) { // explicit check, by default, look for pragma + pragmas(state); // use pragmas to determine parsing behaviour + } // tokenize and initialize a cursor state.tokens = splitFunc.call(state.template, tokenizer); @@ -266,7 +291,7 @@ var Mustache = (function(undefined) { for (i=0, n=optionPairs.length; i': partial, @@ -485,40 +515,49 @@ var Mustache = (function(undefined) { }; function get_variable_name(state, token, prefix, postfix) { - var fragment = token - .substring( - state.openTag.length + (prefix ? 1 : 0) - , token.length - state.closeTag.length - (postfix ? 1 : 0) - ); - + var fragment = token.substring( + state.openTag.length + (prefix ? 1 : 0) + , token.length - state.closeTag.length - (postfix ? 1 : 0) + ); + if (String.prototype.trim) { - return fragment.trim(); + fragment = fragment.trim(); } else { - return fragment.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); + fragment = fragment.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); + } + + if (fragment.indexOf(' ')!==-1) { + throw create_error(state.metrics, 'Malformed variable name "' + fragment + '".'); } + + return fragment; } function change_delimiter(state, token) { var matches = token.match(new RegExp(escape_regex(state.openTag) + '=(\\S*?)\\s*(\\S*?)=' + escape_regex(state.closeTag))); if ((matches || []).length!==3) { - throw create_error(state.line, state.character, 'Malformed change delimiter token: "' + token + '".'); + throw create_error(state.metrics, 'Malformed change delimiter token "' + token + '".'); } var new_state = create_compiler_state( state.tokens.slice(state.cursor+1).join('') , state.partials , matches[1] - , matches[2]); + , matches[2] + , false); new_state.code = state.code; new_state.send_code_func = state.send_code_func; new_state.parser = state.parser; + new_state.metrics.line = state.metrics.line; + new_state.metrics.character = state.metrics.character + token.length; + new_state.metrics.partial = state.metrics.partial; new_state.section = state.section; if (new_state.section) { new_state.section.template_buffer.push(token); } - state.cursor = state.tokens.length; // finish off this level + state.terminated = true; // finish off this level compile(new_state, true); } @@ -534,6 +573,11 @@ var Mustache = (function(undefined) { , template_buffer: [] , inverted: inverted , child_sections: [] + , metrics: { + partial: state.metrics.partial + , line: state.metrics.line + , character: state.metrics.character + token.length + } }; } else { state.section.child_sections.push(variable); @@ -548,17 +592,20 @@ var Mustache = (function(undefined) { function end_section(state, token) { var variable = get_variable_name(state, token, true); - if (state.section.child_sections.length > 0 && - state.section.child_sections[state.section.child_sections.length-1] === variable) { - - state.section.child_sections.pop(); - state.section.template_buffer.push(token); + if (state.section.child_sections.length > 0) { + var child_section = state.section.child_sections[state.section.child_sections.length-1]; + if (child_section === variable) { + state.section.child_sections.pop(); + state.section.template_buffer.push(token); + } else { + throw create_error(state.metrics, 'Unexpected section end tag "' + variable + '", expected "' + child_section + '".'); + } } else if (state.section.variable===variable) { section(state); delete state.section; state.parser = default_parser; } else { - throw create_error(state.line, state.character, 'Unexpected section end tag "' + variable + '", expected "' + state.section.variable + '".'); + throw create_error(state.metrics, 'Unexpected section end tag "' + variable + '", expected "' + state.section.variable + '".'); } } diff --git a/test/unit.js b/test/unit.js index e372e08..2907b62 100644 --- a/test/unit.js +++ b/test/unit.js @@ -52,11 +52,22 @@ test("Parser", function() { {} ); }, function(e) { - return e.message === '(1,1): Malformed change delimiter token: "{{=tag1}}".'; + return e.message === '(1,1): Malformed change delimiter token "{{=tag1}}".'; }, 'Malformed tags should be handled correctly.' ); + raises( + function() { + Mustache.to_html( + '{{tag blah}}' + ) + }, function(e) { + return e.message === '(1,1): Malformed variable name "tag blah".'; + }, + 'Malformed tags should be handled correctly.' + ); + var partials = { 'partial' : '{{key}}' }; Mustache.compile('{{>partial}}', partials ); @@ -477,7 +488,7 @@ test("'%' (Pragmas)", function() { ); }, function(e) { - return e.message === '(1,1): This implementation of mustache does not implement the "I-HAVE-THE-GREATEST-MUSTACHE" pragma.'; + return e.message === 'This implementation of mustache does not implement the "I-HAVE-THE-GREATEST-MUSTACHE" pragma.'; }, 'Notification of unimplemented pragmas' ); @@ -590,6 +601,60 @@ test("Demo", function() { ); }); +test("Error Handling", function() { + raises( + function() { + Mustache.to_html( + 'this is a partial\nyes it is. {{>partial}}', + {}, + {partal: ''} + ); + }, function(e) { + return e.line === 2 && e.character === 12; + }, + 'Missing partial line and character correctness.' + ); + + raises( + function() { + Mustache.to_html( + 'this is a partial\nyes it is. {{>partial}}', + {}, + {partial: 'error in {{#foobar}}'} + ); + }, function(e) { + return e.message === '[partial](1,21): Closing section tag "foobar" expected.' + }, + 'Unbalanced section correctness (Part 1).' + ); + + raises( + function() { + Mustache.to_html( + 'something something something {{/darkside}}.', + {}, + {} + ); + }, function(e) { + return e.message === '(1,31): Unbalanced End Section tag "{{/darkside}}".' + }, + 'Unbalanced section correctness (Part 2).' + ); + + raises( + function() { + Mustache.to_html( + 'this is a partial\nyes it is. {{>maria}}', + {}, + {maria: 'error in {{#foobar}}this is the most aw\ns\nme think {{#evar}}hello joe{{/foobar}}{{/evar}}'} + ); + }, function(e) { + return e.message === '[maria](3,28): Unexpected section end tag "foobar", expected "evar".' + }, + 'Partials metric correctness.' + ); +}); + test("Regression Suite", function() { // matches bug_11_eating_whitespace.html equals(