Skip to content

Commit eee8d69

Browse files
committed
Better error reporting
1 parent 51eec96 commit eee8d69

File tree

2 files changed

+87
-44
lines changed

2 files changed

+87
-44
lines changed

mustache.js

Lines changed: 47 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -380,48 +380,27 @@ var Mustache;
380380
var tree = [];
381381
var collector = tree;
382382
var sections = [];
383-
var token, section;
384383

385-
for (var i = 0; i < tokens.length; ++i) {
384+
var token;
385+
for (var i = 0, len = tokens.length; i < len; ++i) {
386386
token = tokens[i];
387-
388387
switch (token[0]) {
389-
case "#":
390-
case "^":
391-
token[4] = [];
388+
case '#':
389+
case '^':
392390
sections.push(token);
393391
collector.push(token);
394-
collector = token[4];
392+
collector = token[4] = [];
395393
break;
396-
case "/":
397-
if (sections.length === 0) {
398-
throw new Error("Unopened section: " + token[1]);
399-
}
400-
401-
section = sections.pop();
402-
403-
if (section[1] !== token[1]) {
404-
throw new Error("Unclosed section: " + section[1]);
405-
}
406-
407-
if (sections.length > 0) {
408-
collector = sections[sections.length - 1][4];
409-
} else {
410-
collector = tree;
411-
}
394+
case '/':
395+
sections.pop();
396+
var length = sections.length;
397+
collector = length > 0 ? sections[length - 1][4] : tree;
412398
break;
413399
default:
414400
collector.push(token);
415401
}
416402
}
417403

418-
// Make sure there were no open sections when we're done.
419-
section = sections.pop();
420-
421-
if (section) {
422-
throw new Error("Unclosed section: " + section[1]);
423-
}
424-
425404
return tree;
426405
}
427406

@@ -435,7 +414,7 @@ var Mustache;
435414
var token, lastToken;
436415
for (var i = 0, len = tokens.length; i < len; ++i) {
437416
token = tokens[i];
438-
if (lastToken && lastToken[0] === "text" && token[0] === "text") {
417+
if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
439418
lastToken[1] += token[1];
440419
lastToken[3] = token[3];
441420
} else {
@@ -448,10 +427,6 @@ var Mustache;
448427
}
449428

450429
function escapeTags(tags) {
451-
if (tags.length !== 2) {
452-
throw new Error("Invalid tags: " + tags.join(" "));
453-
}
454-
455430
return [
456431
new RegExp(escapeRe(tags[0]) + "\\s*"),
457432
new RegExp("\\s*" + escapeRe(tags[1]))
@@ -468,9 +443,15 @@ var Mustache;
468443
template = template || '';
469444
tags = tags || exports.tags;
470445

446+
if (typeof tags === 'string') tags = tags.split(spaceRe);
447+
if (tags.length !== 2) {
448+
throw new Error('Invalid tags: ' + tags.join(', '));
449+
}
450+
471451
var tagRes = escapeTags(tags);
472452
var scanner = new Scanner(template);
473453

454+
var sections = []; // Stack to hold section tokens
474455
var tokens = []; // Buffer to hold the tokens
475456
var spaces = []; // Indices of whitespace tokens on the current line
476457
var hasTag = false; // Is there a {{tag}} on the current line?
@@ -492,7 +473,6 @@ var Mustache;
492473
}
493474

494475
var start, type, value, chr;
495-
496476
while (!scanner.eos()) {
497477
start = scanner.pos;
498478
value = scanner.scanUntil(tagRes[0]);
@@ -546,25 +526,48 @@ var Mustache;
546526

547527
// Match the closing tag.
548528
if (!scanner.scan(tagRes[1])) {
549-
throw new Error("Unclosed tag at " + scanner.pos);
529+
throw new Error('Unclosed tag at ' + scanner.pos);
550530
}
551531

552-
tokens.push([type, value, start, scanner.pos]);
532+
// Check section nesting.
533+
if (type === '/') {
534+
if (sections.length === 0) {
535+
throw new Error('Unopened section "' + value + '" at ' + start);
536+
}
553537

554-
if (type === "name" || type === "{" || type === "&") {
555-
nonSpace = true;
538+
var section = sections.pop();
539+
540+
if (section[1] !== value) {
541+
throw new Error('Unclosed section "' + section[1] + '" at ' + start);
542+
}
556543
}
557544

558-
// Set the tags for the next time around.
559-
if (type === "=") {
545+
var token = [type, value, start, scanner.pos];
546+
tokens.push(token);
547+
548+
if (type === '#' || type === '^') {
549+
sections.push(token);
550+
} else if (type === "name" || type === "{" || type === "&") {
551+
nonSpace = true;
552+
} else if (type === "=") {
553+
// Set the tags for the next time around.
560554
tags = value.split(spaceRe);
555+
556+
if (tags.length !== 2) {
557+
throw new Error('Invalid tags at ' + start + ': ' + tags.join(', '));
558+
}
559+
561560
tagRes = escapeTags(tags);
562561
}
563562
}
564563

565-
tokens = squashTokens(tokens);
564+
// Make sure there are no open sections when we're done.
565+
var section = sections.pop();
566+
if (section) {
567+
throw new Error('Unclosed section "' + section[1] + '" at ' + scanner.pos);
568+
}
566569

567-
return nestTokens(tokens);
570+
return nestTokens(squashTokens(tokens));
568571
};
569572

570573
// The high-level clearCache, compile, compilePartial, and render functions

test/parse-test.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,44 @@ describe('Mustache.parse', function () {
6363
})(template, expectations[template]);
6464
}
6565

66+
describe('when there is an unclosed tag', function () {
67+
it('throws an error', function () {
68+
assert.throws(function () {
69+
Mustache.parse('My name is {{name');
70+
}, /unclosed tag at 17/i);
71+
});
72+
});
73+
74+
describe('when there is an unclosed section', function () {
75+
it('throws an error', function () {
76+
assert.throws(function () {
77+
Mustache.parse('A list: {{#people}}{{name}}');
78+
}, /unclosed section "people" at 27/i);
79+
});
80+
});
81+
82+
describe('when there is an unopened section', function () {
83+
it('throws an error', function () {
84+
assert.throws(function () {
85+
Mustache.parse('The end of the list! {{/people}}');
86+
}, /unopened section "people" at 21/i);
87+
});
88+
});
89+
90+
describe('when invalid tags are given as an argument', function () {
91+
it('throws an error', function () {
92+
assert.throws(function () {
93+
Mustache.parse('A template <% name %>', [ '<%' ]);
94+
}, /invalid tags/i);
95+
});
96+
});
97+
98+
describe('when the template contains invalid tags', function () {
99+
it('throws an error', function () {
100+
assert.throws(function () {
101+
Mustache.parse('A template {{=<%=}}');
102+
}, /invalid tags at 11/i);
103+
});
104+
});
105+
66106
});

0 commit comments

Comments
 (0)