diff --git a/index.js b/index.js index b979807a9..ee9c06f48 100644 --- a/index.js +++ b/index.js @@ -47,7 +47,7 @@ var _engine = { .then(() => this.parse(html)) .then(tpl => this.render(tpl, ctx, opts)) .catch(e => { - if (e instanceof Errors.RenderBreak) { + if (e instanceof Errors.RenderBreakError) { return e.html; } throw e; @@ -145,7 +145,7 @@ factory.evalValue = Syntax.evalValue; factory.Types = { ParseError: Errors.ParseError, TokenizationEroor: Errors.TokenizationError, - RenderBreak: Errors.RenderBreak, + RenderBreakError: Errors.RenderBreakError, AssertionError: Errors.AssertionError }; diff --git a/src/parser.js b/src/parser.js index 016ef51d3..ee49e73a8 100644 --- a/src/parser.js +++ b/src/parser.js @@ -52,16 +52,18 @@ module.exports = function(Tag, Filter) { function parseToken(token, tokens) { try { - switch (token.type) { - case 'tag': - return parseTag(token, tokens); - case 'output': - return parseOutput(token.value); - case 'html': - return token; + var tpl = null; + if (token.type === 'tag') { + tpl = parseTag(token, tokens); + } else if (token.type === 'output') { + tpl = parseOutput(token.value); + } else { // token.type === 'html' + tpl = token; } + tpl.token = token; + return tpl; } catch (e) { - throw new ParseError(e.message, token.input, token.line, e); + throw new ParseError(e, token); } } @@ -78,7 +80,7 @@ module.exports = function(Tag, Filter) { str = str.substr(match.index + match[0].length); var filters = []; - while(match = lexical.filter.exec(str)){ + while (match = lexical.filter.exec(str)) { filters.push([match[0].trim()]); } @@ -95,6 +97,9 @@ module.exports = function(Tag, Filter) { } return { - parse, parseTag, parseStream, parseOutput + parse, + parseTag, + parseStream, + parseOutput }; }; diff --git a/src/render.js b/src/render.js index bb378e363..27cedd889 100644 --- a/src/render.js +++ b/src/render.js @@ -1,9 +1,9 @@ const Syntax = require('./syntax.js'); const Promise = require('any-promise'); const mapSeries = require('./util/promise.js').mapSeries; -const RenderBreak = require('./util/error.js').RenderBreak; +const RenderBreakError = require('./util/error.js').RenderBreakError; +const RenderError = require('./util/error.js').RenderError; const assert = require('./util/assert.js'); -const _ = require('./util/underscore.js'); var render = { @@ -15,10 +15,11 @@ var render = { return renderTemplate.call(this, tpl) .then(partial => html += partial) .catch(e => { - if(e instanceof RenderBreak){ + if(e instanceof RenderBreakError){ e.resolvedHTML = html; + throw e; } - throw e; + throw new RenderError(e, tpl); }); }).then(() => html); @@ -27,7 +28,8 @@ var render = { return this.renderTag(template, scope) .then(partial => partial === undefined ? '' : partial); } else if (template.type === 'output') { - return Promise.resolve(this.evalOutput(template, scope)) + return Promise.resolve() + .then(() => this.evalOutput(template, scope)) .then(partial => partial === undefined ? '' : stringify(partial)); } else { // template.type === 'html' return Promise.resolve(template.value); @@ -37,10 +39,10 @@ var render = { renderTag: function(template, scope) { if (template.name === 'continue') { - return Promise.reject(new RenderBreak('continue')); + return Promise.reject(new RenderBreakError('continue')); } if (template.name === 'break') { - return Promise.reject(new RenderBreak('break')); + return Promise.reject(new RenderBreakError('break')); } return template.render(scope); }, @@ -53,7 +55,7 @@ var render = { if (scope.get('liquid.strict_filters')) { throw filter.error; } else { - val = '' + val = ''; return true; } } diff --git a/src/tag.js b/src/tag.js index 227ebc67b..932357d27 100644 --- a/src/tag.js +++ b/src/tag.js @@ -1,10 +1,12 @@ const lexical = require('./lexical.js'); +const _ = require('./util/underscore.js'); const Promise = require('any-promise'); const Syntax = require('./syntax.js'); const assert = require('./util/assert.js'); function hash(markup, scope) { - var obj = {}, match; + var obj = {}, + match; lexical.hashCapture.lastIndex = 0; while (match = lexical.hashCapture.exec(markup)) { var k = match[1], @@ -20,9 +22,22 @@ module.exports = function() { var _tagInstance = { render: function(scope) { var obj = hash(this.token.args, scope); - return this.tagImpl.render && this.tagImpl.render(scope, obj) || Promise.resolve(''); + var impl = this.tagImpl; + if (typeof impl.render !== 'function') { + return Promise.resolve(''); + } + return Promise.resolve() + .then(() => typeof impl.render === 'function' ? + impl.render(scope, obj) : '') + .catch(function(e) { + if (_.isError(e)) { + throw e; + } + var msg = `Please reject with an Error in ${impl.render}, got ${e}`; + throw new Error(msg); + }); }, - parse: function(token, tokens){ + parse: function(token, tokens) { this.type = 'tag'; this.token = token; this.name = token.name; @@ -30,7 +45,7 @@ module.exports = function() { var tagImpl = tagImpls[this.name]; assert(tagImpl, `tag ${this.name} not found`); this.tagImpl = Object.create(tagImpl); - if(this.tagImpl.parse){ + if (this.tagImpl.parse) { this.tagImpl.parse(token, tokens); } } @@ -51,6 +66,8 @@ module.exports = function() { } return { - construct, register, clear + construct, + register, + clear }; }; diff --git a/src/tokenizer.js b/src/tokenizer.js index 61c783660..8cba24bee 100644 --- a/src/tokenizer.js +++ b/src/tokenizer.js @@ -4,9 +4,9 @@ const _ = require('./util/underscore.js'); const assert = require('../src/util/assert.js'); function parse(html) { - var tokens = []; - assert(_.isString(html), new TokenizationError('illegal input type')); + assert(_.isString(html), 'illegal input type'); + var tokens = []; var syntax = /({%(.*?)%})|({{(.*?)}})/g; var result, htmlFragment, token; var lastMatchEnd = 0, lastMatchBegin = -1, parsedLinesCount = 0; @@ -27,8 +27,7 @@ function parse(html) { var match = token.value.match(lexical.tagLine); if (!match) { - throw new TokenizationError(`illegal tag: ${token.raw}`, - token.input, token.line); + throw new TokenizationError(`illegal tag syntax`, token); } token.name = match[1]; token.args = match[2]; @@ -36,8 +35,7 @@ function parse(html) { tokens.push(token); } // output - else { - token = factory('output', 3, result); + else { token = factory('output', 3, result); tokens.push(token); } lastMatchEnd = syntax.lastIndex; @@ -60,17 +58,10 @@ function parse(html) { raw: match[offset], value: match[offset + 1].trim(), line: getLineNum(match), - input: getLineContent(match) + input: html }; } - function getLineContent(match) { - var idx1 = match.input.lastIndexOf('\n', match.index); - var idx2 = match.input.indexOf('\n', match.index); - if (idx2 === -1) idx2 = match.input.length; - return match.input.slice(idx1 + 1, idx2); - } - function getLineNum(match) { var lines = match.input.slice(lastMatchBegin + 1, match.index).split('\n'); parsedLinesCount += lines.length - 1; diff --git a/src/util/error.js b/src/util/error.js index dcb02eeb5..7bd734286 100644 --- a/src/util/error.js +++ b/src/util/error.js @@ -1,46 +1,58 @@ -function TokenizationError(message, input, line) { - if(Error.captureStackTrace){ +const _ = require('./underscore.js'); + +function TokenizationError(message, token) { + if (Error.captureStackTrace) { Error.captureStackTrace(this, this.constructor); } this.name = this.constructor.name; - this.message = message; - this.input = input; - this.line = line; + this.input = token.input; + this.line = token.line; + + var context = mkContext(token.input, token.line); + this.message = message + '\n' + context; } TokenizationError.prototype = Object.create(Error.prototype); TokenizationError.prototype.constructor = TokenizationError; -function ParseError(message, input, line, e) { - if(Error.captureStackTrace){ - Error.captureStackTrace(this, this.constructor); - } else{ - this.stack = ""; - } - this.stack += (this.stack ? "\nFrom " : "From ") + e.stack; - +function ParseError(e, token) { this.name = this.constructor.name; - this.originalError = e; + this.stack = e.stack; - this.message = message; - this.input = input; - this.line = line; + this.input = token.input; + this.line = token.line; + + var context = mkContext(token.input, token.line); + this.message = e.message + '\n' + context; } ParseError.prototype = Object.create(Error.prototype); ParseError.prototype.constructor = ParseError; -function RenderBreak(message){ - if(Error.captureStackTrace){ +function RenderError(e, tpl) { + this.name = this.constructor.name; + this.stack = e.stack; + + this.input = tpl.token.input; + this.line = tpl.token.line; + + var context = mkContext(tpl.token.input, tpl.token.line); + this.message = e.message + '\n' + context; +} +RenderError.prototype = Object.create(Error.prototype); +RenderError.prototype.constructor = RenderError; + +function RenderBreakError(message) { + if (Error.captureStackTrace) { Error.captureStackTrace(this, this.constructor); } this.name = this.constructor.name; - this.message = message; + this.message = message || ''; } -RenderBreak.prototype = Object.create(Error.prototype); -RenderBreak.prototype.constructor = RenderBreak; +RenderBreakError.prototype = Object.create(Error.prototype); +RenderBreakError.prototype.constructor = RenderBreakError; -function AssertionError(message){ - if(Error.captureStackTrace){ +function AssertionError(message) { + if (Error.captureStackTrace) { Error.captureStackTrace(this, this.constructor); } this.name = this.constructor.name; @@ -49,6 +61,35 @@ function AssertionError(message){ AssertionError.prototype = Object.create(Error.prototype); AssertionError.prototype.constructor = AssertionError; +function mkContext(input, line) { + var lines = input.split('\n'); + var begin = Math.max(line - 2, 1); + var end = Math.min(line + 3, lines.length); + + var context = _ + .range(begin, end + 1) + .map(l => [ + (l === line) ? '>> ' : ' ', + align(l, end), + '| ', + lines[l - 1] + ].join('')) + .join('\n'); + + return context; +} + +function align(n, max) { + var length = (max + '').length; + var str = n + ''; + var blank = Array(length - str.length).join(' '); + return blank + str; +} + module.exports = { - TokenizationError, ParseError, RenderBreak, AssertionError + TokenizationError, + ParseError, + RenderBreakError, + AssertionError, + RenderError }; diff --git a/src/util/underscore.js b/src/util/underscore.js index 67744f421..d4194852b 100644 --- a/src/util/underscore.js +++ b/src/util/underscore.js @@ -7,6 +7,13 @@ function isString(value) { return value instanceof String || typeof value === 'string'; } +function isError(value) { + var signature = Object.prototype.toString.call(value); + // [object XXXError] + return signature.substr(-6, 5) === 'Error' || + (typeof value.message == 'string' && typeof value.name == 'string'); +} + /* * Iterates over own enumerable string keyed properties of an object and invokes iteratee for each property. * The iteratee is invoked with three arguments: (value, key, object). @@ -88,9 +95,34 @@ function isObject(value) { return value !== null && typeof value === 'object'; } +/* + * A function to create flexibly-numbered lists of integers, + * handy for each and map loops. start, if omitted, defaults to 0; step defaults to 1. + * Returns a list of integers from start (inclusive) to stop (exclusive), + * incremented (or decremented) by step, exclusive. + * Note that ranges that stop before they start are considered to be zero-length instead of + * negative — if you'd like a negative range, use a negative step. + */ +function range(start, stop, step) { + if (arguments.length === 1) { + stop = start; + start = 0; + } + step = step || 1; + + var arr = []; + for (var i = start; i < stop; i += step) { + arr.push(i); + } + return arr; +} + exports.isString = isString; exports.isArray = isArray; exports.isObject = isObject; +exports.isError = isError; + +exports.range = range; exports.forOwn = forOwn; exports.assign = assign; diff --git a/tags/for.js b/tags/for.js index d5fd8c136..97f0f18a7 100644 --- a/tags/for.js +++ b/tags/for.js @@ -1,8 +1,7 @@ const Liquid = require('..'); -const Promise = require('any-promise'); const lexical = Liquid.lexical; const mapSeries = require('../src/util/promise.js').mapSeries; -const RenderBreak = Liquid.Types.RenderBreak; +const RenderBreakError = Liquid.Types.RenderBreakError; const assert = require('../src/util/assert.js'); const re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+` + `(${lexical.value.source})` + @@ -23,11 +22,11 @@ module.exports = function(liquid) { this.elseTemplates = []; var p, stream = liquid.parser.parseStream(remainTokens) - .on('start', x => p = this.templates) - .on('tag:else', token => p = this.elseTemplates) - .on('tag:endfor', token => stream.stop()) + .on('start', () => p = this.templates) + .on('tag:else', () => p = this.elseTemplates) + .on('tag:endfor', () => stream.stop()) .on('template', tpl => p.push(tpl)) - .on('end', x => { + .on('end', () => { throw new Error(`tag ${tagToken.raw} not closed`); }); @@ -71,7 +70,7 @@ module.exports = function(liquid) { .renderTemplates(this.templates, scope) .then(partial => html += partial) .catch(e => { - if (e instanceof RenderBreak) { + if (e instanceof RenderBreakError) { html += e.resolvedHTML; if (e.message === 'continue') return; } @@ -79,7 +78,7 @@ module.exports = function(liquid) { }) .then(() => scope.pop()); }).catch((e) => { - if (e instanceof RenderBreak && e.message === 'break') { + if (e instanceof RenderBreakError && e.message === 'break') { return; } throw e; diff --git a/test/tag.js b/test/tag.js index a07739054..7b16f827f 100644 --- a/test/tag.js +++ b/test/tag.js @@ -41,12 +41,14 @@ describe('tag', function() { tag.register('foo', { render: spy }); - tag.construct({ - type: 'tag', - value: 'foo', - name: 'foo' - }, []).render(scope, {}); - expect(spy).to.have.been.called; + return tag + .construct({ + type: 'tag', + value: 'foo', + name: 'foo' + }, []) + .render(scope, {}) + .then(() => expect(spy).to.have.been.called); }); it('should call tag.render with resolved hash', function() { @@ -61,11 +63,11 @@ describe('tag', function() { name: 'foo', args: 'aa:foo bb: arr[0] cc: 2.3' }; - tag.construct(token, []).render(scope, {}); - expect(spy).to.have.been.calledWithMatch(scope, { - aa: 'bar', - bb: 2, - cc: 2.3 - }); + return tag.construct(token, []).render(scope, {}) + .then(() => expect(spy).to.have.been.calledWithMatch(scope, { + aa: 'bar', + bb: 2, + cc: 2.3 + })); }); }); diff --git a/test/tags/for.js b/test/tags/for.js index b9afd7a5b..fc26b15bc 100644 --- a/test/tags/for.js +++ b/test/tags/for.js @@ -52,27 +52,30 @@ describe('tags/for', function() { .to.eventually.equal('12345'); }); it('should support for with break', function() { - src = '{% for i in (one..5) %}' + + var src = '{% for i in (one..5) %}' + '{% if i == 4 %}{% break %}{% endif %}' + '{{ i }}' + '{% endfor %}'; + //return liquid.parseAndRender(src, ctx).catch(e => { + //console.log(e.stack); + //}); return expect(liquid.parseAndRender(src, ctx)) .to.eventually.equal('123'); }); it('should support for with limit', function() { - src = '{% for i in (1..5) limit:2 %}{{ i }}{% endfor %}'; + var src = '{% for i in (1..5) limit:2 %}{{ i }}{% endfor %}'; return expect(liquid.parseAndRender(src, ctx)) .to.eventually.equal('12'); }); it('should support for with limit and offset', function() { - src = '{% for i in (1..10) limit:2 offset:5%}{{ i }}{% endfor %}'; + var src = '{% for i in (1..10) limit:2 offset:5%}{{ i }}{% endfor %}'; return expect(liquid.parseAndRender(src, ctx)) .to.eventually.equal('67'); }); it('should support for reversed', function() { - src = '{% for i in (1..5) limit:2 reversed %}{{ i }}{% endfor %}'; + var src = '{% for i in (1..5) limit:2 reversed %}{{ i }}{% endfor %}'; return expect(liquid.parseAndRender(src, ctx)) .to.eventually.equal('21'); }); diff --git a/test/util/error.js b/test/util/error.js index b643dc876..2ba04d28a 100644 --- a/test/util/error.js +++ b/test/util/error.js @@ -13,92 +13,310 @@ describe('error', function() { describe('TokenizationError', function() { it('should throw TokenizationError when tag illegal', function() { - return engine.parseAndRender('{% . a %}', {}).catch(function(err) { - expect(err.name).to.equal('TokenizationError'); - expect(err.message).to.equal('illegal tag: {% . a %}'); - expect(err.input).to.equal('{% . a %}'); - expect(err.line).to.equal(1); - }); + return expect(engine.parseAndRender('{% . a %}', {})).to.eventually + .be.rejected + .then(function(err) { + expect(err.name).to.equal('TokenizationError'); + expect(err.message).to.contain('illegal tag syntax'); + }); }); - it('should throw TokenizationError when tag syntax illegal', function() { - return engine.parseAndRender('{% . a }', {}).catch(function(err) { - expect(err.name).to.equal('TokenizationError'); - expect(err.message).to.equal('illegal tag: {% . a }'); - expect(err.input).to.equal('{% . a }'); - expect(err.line).to.equal(1); - }); + it('should contain template content in err.message', function() { + var html = ['1st', '2nd', 'X{% . a %} Y', '4th']; + var message = [ + 'illegal tag syntax', + ' 1| 1st', + ' 2| 2nd', + '>> 3| X{% . a %} Y', + ' 4| 4th' + ]; + return expect(engine.parseAndRender(html.join('\n'))).to.eventually + .be.rejected + .then(function(err) { + expect(err.message).to.equal(message.join('\n')); + expect(err.name).to.equal('TokenizationError'); + }); }); - it('should throw TokenizationError when filter syntax illegal', function() { - return engine.parseAndRender('{{ a|| }}', {}).catch(function(err) { - expect(err.name).to.equal('TokenizationError'); - expect(err.message).to.equal('{{ a|| }'); - expect(err.input).to.equal('{{ a|| }'); - expect(err.line).to.equal(1); + it('should contain the whole template content in err.input', function() { + var html = 'bar\nfoo{% . a %}\nfoo'; + return expect(engine.parseAndRender(html)).to.eventually + .be.rejected + .then(function(err) { + expect(err.input).to.equal(html); + }); + }); + it('should contain line number in err.line', function() { + return expect(engine.parseAndRender('1\n2\n{% . a %}\n4', {})).to.eventually + .be.rejected + .then(function(err) { + expect(err.name).to.equal('TokenizationError'); + expect(err.line).to.equal(3); + }); + }); + it('should contain stack in err.stack', function() { + return expect(engine.parseAndRender('{% . a %}')).to.eventually + .be.rejected + .then(function(err) { + expect(err.stack).to.contain('illegal tag syntax'); + expect(err.stack).to.contain('at Object.parse'); + }); + }); + it('should contain file path in err.file', function() { + var html = '\n\n\n{% . a %}\n\n'; + mock({ + "/foo.html": html }); + return expect(engine.renderFile('/foo.html')).to.eventually + .be.rejected + .then(function(err) { + mock.restore(); + expect(err.name).to.equal('TokenizationError'); + expect(err.file).to.equal('/foo.html'); + }); }); }); - describe('TypeError', function() { + describe('RenderError', function() { + beforeEach(function() { + engine = require('../..')(); + engine.registerTag('throwingTag', { + render: function() { + throw new Error('intended render error'); + } + }); + engine.registerTag('rejectingTag', { + render: function() { + return Promise.reject(new Error('intended render reject')); + } + }); + engine.registerFilter('throwingFilter', () => { + throw new Error('throwed by filter'); + }); + }); + it('should throw RenderError when tag throws', function() { + var src = '{%throwingTag%}'; + return expect(engine.parseAndRender(src)).to.eventually + .be.rejected + .then(function(err) { + expect(err.name).to.equal('RenderError'); + expect(err.message).to.contain('intended render error'); + }); + }); + it('should throw RenderError when tag rejects', function() { + var src = '{%rejectingTag%}'; + return expect(engine.parseAndRender(src)).to.eventually + .be.rejected + .then(function(err) { + expect(err.name).to.equal('RenderError'); + expect(err.message).to.contain('intended render reject'); + }); + }); + it('should throw RenderError when filter throws', function() { + var src = '{{1|throwingFilter}}'; + return expect(engine.parseAndRender(src)).to.eventually + .be.rejected + .then(function(err) { + expect(err.name).to.equal('RenderError'); + expect(err.message).to.contain('throwed by filter'); + }); + }); it('should not throw when variable undefined by default', function() { return expect(engine.parseAndRender('X{{a}}Y')).to.eventually.equal('XY'); }); - it('should throw TypeError when variable not defined', function() { + it('should throw RenderError when variable not defined', function() { return expect(strictEngine.parseAndRender('{{a}}')).to.eventually .be.rejected - .then(function(e){ - expect(e).to.have.property('name', 'TypeError'); - expect(e).to.have.property('message', 'undefined variable: a'); + .then(function(e) { + expect(e).to.have.property('name', 'RenderError'); + expect(e.message).to.contain('undefined variable: a'); }); }); - it('should throw TypeError when filter not defined', function() { + it('should throw RenderError when filter not defined', function() { return expect(strictEngine.parseAndRender('{{1 | a}}')).to.eventually .be.rejected - .then(function(e){ - expect(e).to.have.property('name', 'TypeError'); - expect(e).to.have.property('message', 'undefined filter: a'); + .then(function(e) { + expect(e).to.have.property('name', 'RenderError'); + expect(e.message).to.contain('undefined filter: a'); + }); + }); + it('should contain template content in err.message', function() { + var html = ['1st', '2nd', '3rd', 'X{%throwingTag%} Y', '5th', '6th', '7th']; + var message = [ + 'intended render error', + ' 2| 2nd', + ' 3| 3rd', + '>> 4| X{%throwingTag%} Y', + ' 5| 5th', + ' 6| 6th', + ' 7| 7th' + ]; + return expect(engine.parseAndRender(html.join('\n'))).to.eventually + .be.rejected + .then(function(err) { + expect(err.message).to.equal(message.join('\n')); + expect(err.name).to.equal('RenderError'); + }); + }); + it('should contain the whole template content in err.input', function() { + var html = 'bar\nfoo{%throwingTag%}\nfoo'; + return expect(engine.parseAndRender(html)).to.eventually + .be.rejected + .then(function(err) { + expect(err.input).to.equal(html); + expect(err.name).to.equal('RenderError'); + }); + }); + it('should contain line number in err.line', function() { + var src = '1\n2\n{{1|throwingFilter}}\n4'; + return expect(engine.parseAndRender(src)).to.eventually + .be.rejected + .then(function(err) { + expect(err.line).to.equal(3); + expect(err.name).to.equal('RenderError'); + }); + }); + it('should contain stack in err.stack', function() { + return expect(engine.parseAndRender('{%rejectingTag%}')).to.eventually + .be.rejected + .then(function(err) { + expect(err.stack).to.contain('intended render reject'); + expect(err.stack).to.contain('at Object.engine.registerTag.render'); + }); + }); + + it('should contain file path in err.file', function() { + var html = '\n\n\n{% throwingTag %}\n\n'; + mock({ + "/foo.html": html + }); + return expect(engine.renderFile('/foo.html')).to.eventually + .be.rejected + .then(function(err) { + mock.restore(); + expect(err.name).to.equal('RenderError'); + expect(err.file).to.equal('/foo.html'); }); }); }); describe('ParseError', function() { - it('should throw correct error info', function() { - var src = '{%if true%}\naaa{%endif%}\n{% -a %}\n3'; - return engine.parseAndRender(src).catch(function(err) { - expect(err.name).to.equal('ParseError'); - expect(err.input).to.equal('{% -a %}'); - expect(err.line).to.equal(3) - expect(err.stack).to.contain('From AssertionError: tag -a not found'); + beforeEach(function() { + engine = require('../..')(); + engine.registerTag('throwsOnParse', { + parse: function() { + throw new Error('intended parse error'); + } }); }); - it('should throw correct error info for files', function() { - mock({ - "/foo.html": '\n\n\n{% raw %}\n\n' - }); - return engine.renderFile('/foo.html').catch(function(err) { - mock.restore(); - expect(err.name).to.equal('ParseError'); - expect(err.input).to.equal('{% raw %}'); - expect(err.line).to.equal(4); - expect(err.file).to.equal('/foo.html'); - }); + it('should throw ParseError when tag not closed', function() { + return expect(engine.parseAndRender('{% if %}')).to.eventually + .be.rejected + .then(function(err) { + expect(err.name).to.equal('ParseError'); + expect(err.message).to.contain('tag {% if %} not closed'); + }); + }); + it('should throw ParseError when tag parse throws', function() { + var src = '{%throwsOnParse%}'; + return expect(engine.parseAndRender(src)).to.eventually + .be.rejected + .then(function(err) { + expect(err.name).to.equal('ParseError'); + expect(err.message).to.contain('intended parse error'); + }); + }); + it('should throw ParseError when tag not found', function() { + var src = '{%if true%}\naaa{%endif%}\n{% -a %}\n3'; + return expect(engine.parseAndRender(src)).to.eventually + .be.rejected + .then(function(err) { + expect(err.name).to.equal('ParseError'); + expect(err.message).to.contain('tag -a not found'); + }); }); it('should throw ParseError when tag not exist', function() { - return engine.parseAndRender('{% a %}').catch(function(err) { - expect(err.name).to.equal('ParseError'); - expect(err.message).to.equal('tag a not found'); - expect(err.input).to.equal('{% a %}'); - expect(err.line).to.equal(1); - }); + return expect(engine.parseAndRender('{% a %}')).to.eventually + .be.rejected + .then(function(err) { + expect(err.name).to.equal('ParseError'); + expect(err.message).to.contain('tag a not found'); + }); }); - it('should throw ParseError when tag not closed', function() { - return engine.parseAndRender('{% if %}').catch(function(err) { - expect(err.name).to.equal('ParseError'); - expect(err.message).to.equal('tag {% if %} not closed'); - expect(err.input).to.equal('{% if %}'); - expect(err.line).to.equal(1); + it('should contain template content in err.message', function() { + var html = ['1st', '2nd', '3rd', 'X{% a %} {% enda %} Y', '5th', '6th', '7th']; + var message = [ + 'tag a not found', + ' 2| 2nd', + ' 3| 3rd', + '>> 4| X{% a %} {% enda %} Y', + ' 5| 5th', + ' 6| 6th', + ' 7| 7th' + ]; + return expect(engine.parseAndRender(html.join('\n'))).to.eventually + .be.rejected + .then(function(err) { + expect(err.message).to.equal(message.join('\n')); + expect(err.name).to.equal('ParseError'); + }); + }); + + it('should handle err.message when context not enough', function() { + var html = ['1st', 'X{% a %} {% enda %} Y', '3rd', '4th']; + var message = [ + 'tag a not found', + ' 1| 1st', + '>> 2| X{% a %} {% enda %} Y', + ' 3| 3rd', + ' 4| 4th' + ]; + return expect(engine.parseAndRender(html.join('\n'))).to.eventually + .be.rejected + .then(function(err) { + expect(err.message).to.equal(message.join('\n')); + }); + }); + + it('should contain the whole template content in err.input', function() { + var html = 'bar\nfoo{% a %}\nfoo'; + return expect(engine.parseAndRender(html)).to.eventually + .be.rejected + .then(function(err) { + expect(err.input).to.equal(html); + }); + }); + + it('should contain line number in err.line', function() { + var html = '\n\n\n{% raw %}\n\n'; + return expect(engine.parseAndRender(html)).to.eventually + .be.rejected + .then(function(err) { + expect(err.line).to.equal(4); + }); + }); + + it('should contain stack in err.stack', function() { + return expect(engine.parseAndRender('{% -a %}')).to.eventually + .be.rejected + .then(function(err) { + expect(err.stack).to.contain('AssertionError: tag -a not found'); + expect(err.stack).to.contain('at Object._tagInstance.parse'); + }); + }); + + it('should contain file path in err.file', function() { + var html = '\n\n\n{% raw %}\n\n'; + mock({ + "/foo.html": html }); + return expect(engine.renderFile('/foo.html')).to.eventually + .be.rejected + .then(function(err) { + mock.restore(); + expect(err.name).to.equal('ParseError'); + expect(err.file).to.equal('/foo.html'); + }); }); }); }); diff --git a/test/util/underscore.js b/test/util/underscore.js index fad3dd0a1..1acc1d557 100644 --- a/test/util/underscore.js +++ b/test/util/underscore.js @@ -1,11 +1,28 @@ const chai = require("chai"); const sinon = require('sinon'); const expect = chai.expect; +const Errors = require('../../src/util/error.js'); chai.use(require("sinon-chai")); var _ = require('../../src/util/underscore.js'); describe('util/underscore', function() { + describe('.isError()', function() { + it('should return true for new Error', function() { + expect(_.isError(new Error())).to.be.true; + }); + it('should return true for RenderError', function() { + var tpl = { + token: { + input: 'xx' + } + }; + expect(_.isError(new Errors.RenderError(new Error(), tpl))).to.be.true; + }); + it('should return true for RenderBreakError', function() { + expect(_.isError(new Errors.RenderBreakError())).to.be.true; + }); + }); describe('.isString()', function() { it('should return true for literal string', function() { expect(_.isString('foo')).to.be.true;