Compare commits

...
13 Commits
Author SHA1 Message Date
harttle 2c0ba9bfd9 1.5.3 2016-12-08 23:26:00 +08:00
harttle 89636736a4 robust date filter 2016-12-08 23:25:38 +08:00
harttle deca9677dd modified: README.md 2016-12-05 23:49:07 +08:00
harttle 543368ed89 unit test for original error for {%layout%} 2016-12-01 00:50:36 +08:00
harttle dcd7b01fa4 update tag test 2016-11-16 02:53:30 +08:00
harttle 209d3910ab 1.5.2 2016-11-16 01:07:08 +08:00
harttle 40bdd7e94a refactor: move file to token property 2016-11-16 01:06:37 +08:00
harttle 03b7ee95ed 1.5.1 2016-11-16 00:50:21 +08:00
harttle 64e9cb7044 fix nested render error message 2016-11-16 00:48:54 +08:00
harttle 20af0c489e 1.5.0 2016-11-16 00:32:16 +08:00
harttle 2bc53a3850 change: move context error from err.message to err.stack 2016-11-16 00:31:20 +08:00
harttle 3a450bba1b 1.4.3 2016-11-15 21:58:25 +08:00
harttle 397b2e48b6 fix default filter 2016-11-15 21:58:05 +08:00
11 changed files with 201 additions and 68 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
[![NPM version](https://img.shields.io/npm/v/shopify-liquid.svg?style=flat)](https://www.npmjs.org/package/shopify-liquid)
[![Build Status](https://travis-ci.org/harttle/shopify-liquid.svg?branch=master)](https://travis-ci.org/harttle/shopify-liquid)
[![Coverage Status](https://img.shields.io/coveralls/harttle/shopify-liquid/master.svg)](https://coveralls.io/github/harttle/shopify-liquid?branch=master)
[![Dependency manager](https://img.shields.io/david/harttle/shopify-liquid.svg?style=flat)](https://david-dm.org/harttle/shopify-liquid)
[![Dependency manager](https://david-dm.org/harttle/shopify-liquid.svg?style=flat)](https://david-dm.org/harttle/shopify-liquid)
A feature-rich Liquid template engine for Node.js and browsers,
with compliance with [the Ruby version][shopify-liquid].
+32 -13
View File
@@ -34,10 +34,10 @@ var filters = {
},
'date': function date(v, arg) {
if (v === 'now') v = new Date();
return strftime(v, arg);
return v instanceof Date ? strftime(v, arg) : '';
},
'default': function _default(v, arg) {
return arg || v;
return v || arg;
},
'divided_by': function divided_by(v, arg) {
return Math.floor(v / arg);
@@ -241,8 +241,8 @@ var _engine = {
return this;
},
parse: function parse(html) {
var tokens = tokenizer.parse(html);
parse: function parse(html, filepath) {
var tokens = tokenizer.parse(html, filepath);
return this.parser.parse(tokens);
},
render: function render(tpl, ctx, opts) {
@@ -270,9 +270,6 @@ var _engine = {
opts = _.assign({}, opts);
return this.getTemplate(filepath, opts.root).then(function (templates) {
return _this2.render(templates, ctx, opts);
}).catch(function (e) {
e.file = filepath;
throw e;
});
},
evalOutput: function evalOutput(str, scope) {
@@ -321,7 +318,7 @@ var _engine = {
});
} else {
return readFileAsync(filepath).then(function (str) {
return _this3.parse(str);
return _this3.parse(str, filepath);
});
}
});
@@ -1198,7 +1195,7 @@ var TokenizationError = require('./util/error.js').TokenizationError;
var _ = require('./util/underscore.js');
var assert = require('../src/util/assert.js');
function parse(html) {
function parse(html, filepath) {
assert(_.isString(html), 'illegal input type');
var tokens = [];
@@ -1256,7 +1253,8 @@ function parse(html) {
raw: match[offset],
value: match[offset + 1].trim(),
line: getLineNum(match),
input: html
input: html,
file: filepath
};
}
@@ -1300,9 +1298,11 @@ function TokenizationError(message, token) {
this.input = token.input;
this.line = token.line;
this.file = token.file;
var context = mkContext(token.input, token.line);
this.message = message + '\n' + context;
this.message = mkMessage(message, token);
this.stack = context + '\n' + (this.stack || '');
}
TokenizationError.prototype = Object.create(Error.prototype);
TokenizationError.prototype.constructor = TokenizationError;
@@ -1313,22 +1313,30 @@ function ParseError(e, token) {
this.input = token.input;
this.line = token.line;
this.file = token.file;
var context = mkContext(token.input, token.line);
this.message = e.message + '\n' + context;
this.message = mkMessage(e.message, token);
this.stack = context + '\n' + (this.stack || '');
}
ParseError.prototype = Object.create(Error.prototype);
ParseError.prototype.constructor = ParseError;
function RenderError(e, tpl) {
// return the original render error
if (e instanceof RenderError) {
return e;
}
this.name = this.constructor.name;
this.stack = e.stack;
this.input = tpl.token.input;
this.line = tpl.token.line;
this.file = tpl.token.file;
var context = mkContext(tpl.token.input, tpl.token.line);
this.message = e.message + '\n' + context;
this.message = mkMessage(e.message, tpl.token);
this.stack = context + '\n' + (e.stack || '');
}
RenderError.prototype = Object.create(Error.prototype);
RenderError.prototype.constructor = RenderError;
@@ -1372,6 +1380,17 @@ function align(n, max) {
return blank + str;
}
function mkMessage(msg, token) {
msg = msg || '';
if (token.file) {
msg += ', file:' + token.file;
}
if (token.line) {
msg += ', line:' + token.line;
}
return msg;
}
module.exports = {
TokenizationError: TokenizationError,
ParseError: ParseError,
+2 -2
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -23,9 +23,9 @@ var filters = {
'ceil': v => Math.ceil(v),
'date': (v, arg) => {
if (v === 'now') v = new Date();
return strftime(v, arg);
return v instanceof Date ? strftime(v, arg) : '';
},
'default': (v, arg) => arg || v,
'default': (v, arg) => v || arg,
'divided_by': (v, arg) => Math.floor(v / arg),
'downcase': v => v.toLowerCase(),
'escape': escape,
+4 -8
View File
@@ -33,8 +33,8 @@ var _engine = {
return this;
},
parse: function(html) {
var tokens = tokenizer.parse(html);
parse: function(html, filepath) {
var tokens = tokenizer.parse(html, filepath);
return this.parser.parse(tokens);
},
render: function(tpl, ctx, opts) {
@@ -56,11 +56,7 @@ var _engine = {
renderFile: function(filepath, ctx, opts) {
opts = _.assign({}, opts);
return this.getTemplate(filepath, opts.root)
.then(templates => this.render(templates, ctx, opts))
.catch(e => {
e.file = filepath;
throw e;
});
.then(templates => this.render(templates, ctx, opts));
},
evalOutput: function(str, scope) {
var tpl = this.parser.parseOutput(str.trim());
@@ -100,7 +96,7 @@ var _engine = {
.then(str => this.parse(str))
.then(tpl => this.cache[filepath] = tpl);
} else {
return readFileAsync(filepath).then(str => this.parse(str));
return readFileAsync(filepath).then(str => this.parse(str, filepath));
}
});
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "shopify-liquid",
"version": "1.4.2",
"version": "1.5.3",
"description": "A feature-rich Liquid template engine for Node.js and browsers, with compliance with the Ruby version.",
"main": "index.js",
"scripts": {
+3 -2
View File
@@ -3,7 +3,7 @@ const TokenizationError = require('./util/error.js').TokenizationError;
const _ = require('./util/underscore.js');
const assert = require('../src/util/assert.js');
function parse(html) {
function parse(html, filepath) {
assert(_.isString(html), 'illegal input type');
var tokens = [];
@@ -58,7 +58,8 @@ function parse(html) {
raw: match[offset],
value: match[offset + 1].trim(),
line: getLineNum(match),
input: html
input: html,
file: filepath
};
}
+24 -3
View File
@@ -8,9 +8,11 @@ function TokenizationError(message, token) {
this.input = token.input;
this.line = token.line;
this.file = token.file;
var context = mkContext(token.input, token.line);
this.message = message + '\n' + context;
this.message = mkMessage(message, token);
this.stack = context + '\n' + (this.stack || '');
}
TokenizationError.prototype = Object.create(Error.prototype);
TokenizationError.prototype.constructor = TokenizationError;
@@ -21,22 +23,30 @@ function ParseError(e, token) {
this.input = token.input;
this.line = token.line;
this.file = token.file;
var context = mkContext(token.input, token.line);
this.message = e.message + '\n' + context;
this.message = mkMessage(e.message, token);
this.stack = context + '\n' + (this.stack || '');
}
ParseError.prototype = Object.create(Error.prototype);
ParseError.prototype.constructor = ParseError;
function RenderError(e, tpl) {
// return the original render error
if(e instanceof RenderError){
return e;
}
this.name = this.constructor.name;
this.stack = e.stack;
this.input = tpl.token.input;
this.line = tpl.token.line;
this.file = tpl.token.file;
var context = mkContext(tpl.token.input, tpl.token.line);
this.message = e.message + '\n' + context;
this.message = mkMessage(e.message, tpl.token);
this.stack = context + '\n' + (e.stack || '');
}
RenderError.prototype = Object.create(Error.prototype);
RenderError.prototype.constructor = RenderError;
@@ -86,6 +96,17 @@ function align(n, max) {
return blank + str;
}
function mkMessage(msg, token){
msg = msg || '';
if(token.file){
msg += ', file:' + token.file;
}
if(token.line){
msg += ', line:' + token.line;
}
return msg;
}
module.exports = {
TokenizationError,
ParseError,
+7 -1
View File
@@ -51,9 +51,15 @@ describe('filters', function() {
it('should create a new Date when given "now"', function() {
return test('{{ "now" | date: "%Y"}}', (new Date).getFullYear().toString());
});
it('should render as empty string when invalid', function() {
return test('{{ "" | date: "%Y"}}', "");
});
});
it('should support default', () => test('{{false |default: "a"}}', 'a'));
describe('default', function() {
it('should use default when falsy', () => test('{{false |default: "a"}}', 'a'));
it('should not use default when truthy', () => test('{{true |default: "a"}}', 'true'));
});
describe('divided_by', function() {
it('should return 2 for 4,2', () => test('{{4 | divided_by: 2}}', '2'));
+46 -20
View File
@@ -11,7 +11,10 @@ describe('tag', function() {
before(function() {
scope = Scope.factory({
foo: 'bar',
arr: [2, 1]
arr: [2, 1],
bar: {
coo: 'uoo'
}
});
tag.clear();
});
@@ -36,8 +39,7 @@ describe('tag', function() {
});
it('should call tag.render', function() {
var spy = sinon.spy(),
tokens = [];
var spy = sinon.spy();
tag.register('foo', {
render: spy
});
@@ -51,23 +53,47 @@ describe('tag', function() {
.then(() => expect(spy).to.have.been.called);
});
it('should call tag.render with resolved hash', function() {
var spy = sinon.spy(),
tokens = [];
tag.register('foo', {
render: spy
describe('hash', function(){
var spy, token;
beforeEach(function(){
spy = sinon.spy();
tag.register('foo', {
render: spy
});
token = {
type: 'tag',
value: 'foo aa:foo bb: arr[0] cc: 2.3 dd:bar.coo',
name: 'foo',
args: 'aa:foo bb: arr[0] cc: 2.3 dd:bar.coo'
};
});
it('should call tag.render with scope', function() {
return tag.construct(token, []).render(scope, {})
.then(() => expect(spy).to.have.been.calledWithMatch(scope));
});
it('should resolve identifier hash', function() {
return tag.construct(token, []).render(scope, {})
.then(() => expect(spy).to.have.been.calledWithMatch({}, {
aa: 'bar'
}));
});
it('should accept space between key/value', function() {
return tag.construct(token, []).render(scope, {})
.then(() => expect(spy).to.have.been.calledWithMatch({}, {
bb: 2,
}));
});
it('should resolve number value hash', function() {
return tag.construct(token, []).render(scope, {})
.then(() => expect(spy).to.have.been.calledWithMatch(scope, {
cc: 2.3
}));
});
it('should resolve property access hash', function() {
return tag.construct(token, []).render(scope, {})
.then(() => expect(spy).to.have.been.calledWithMatch(scope, {
dd: 'uoo'
}));
});
var token = {
type: 'tag',
value: 'foo aa:foo bb: arr[0] cc: 2.3',
name: 'foo',
args: 'aa:foo bb: arr[0] cc: 2.3'
};
return tag.construct(token, []).render(scope, {})
.then(() => expect(spy).to.have.been.calledWithMatch(scope, {
aa: 'bar',
bb: 2,
cc: 2.3
}));
});
});
+79 -15
View File
@@ -10,6 +10,9 @@ var strictEngine = require('../..')({
});
describe('error', function() {
afterEach(function() {
mock.restore();
});
describe('TokenizationError', function() {
it('should throw TokenizationError when tag illegal', function() {
@@ -23,16 +26,17 @@ describe('error', function() {
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'
' 4| 4th',
'TokenizationError: illegal tag syntax',
];
return expect(engine.parseAndRender(html.join('\n'))).to.eventually
.be.rejected
.then(function(err) {
expect(err.message).to.equal(message.join('\n'));
expect(err.message).to.equal('illegal tag syntax, line:3');
expect(err.stack).to.contain(message.join('\n'));
expect(err.name).to.equal('TokenizationError');
});
});
@@ -77,7 +81,9 @@ describe('error', function() {
describe('RenderError', function() {
beforeEach(function() {
engine = require('../..')();
engine = require('../..')({
root: '/'
});
engine.registerTag('throwingTag', {
render: function() {
throw new Error('intended render error');
@@ -130,21 +136,77 @@ describe('error', function() {
expect(e.message).to.contain('undefined variable: a');
});
});
it('should contain template content in err.message', function() {
it('should contain template context in err.stack', 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'
' 7| 7th',
'Error: intended render error',
];
return expect(engine.parseAndRender(html.join('\n'))).to.eventually
.be.rejected
.then(function(err) {
expect(err.message).to.equal(message.join('\n'));
expect(err.message).to.equal('intended render error, line:4');
expect(err.stack).to.contain(message.join('\n'));
expect(err.name).to.equal('RenderError');
});
});
it('should contain original error info for {% layout %}', function() {
mock({
'/throwing-tag.html': [
'1st',
'2nd',
'3rd',
'X{%throwingTag%} Y',
'5th',
'{%block%}{%endblock%}',
'7th'
].join('\n')
});
var html = '{%layout "throwing-tag.html"%}';
var message = [
' 2| 2nd',
' 3| 3rd',
'>> 4| X{%throwingTag%} Y',
' 5| 5th',
' 6| {%block%}{%endblock%}',
' 7| 7th',
'Error: intended render error',
];
return expect(engine.parseAndRender(html)).to.eventually
.be.rejected
.then(function(err) {
console.log(err.message);
console.log(err.stack);
expect(err.message).to.equal('intended render error, file:/throwing-tag.html, line:4');
expect(err.stack).to.contain(message.join('\n'));
expect(err.name).to.equal('RenderError');
});
});
it('should contain original error info for {% include %}', function() {
var origin = ['1st', '2nd', '3rd', 'X{%throwingTag%} Y', '5th', '6th', '7th'];
mock({
'/throwing-tag.html': origin.join('\n')
});
var html = '{%include "throwing-tag.html"%}';
var message = [
' 2| 2nd',
' 3| 3rd',
'>> 4| X{%throwingTag%} Y',
' 5| 5th',
' 6| 6th',
' 7| 7th',
'Error: intended render error',
];
return expect(engine.parseAndRender(html)).to.eventually
.be.rejected
.then(function(err) {
expect(err.message).to.equal('intended render error, file:/throwing-tag.html, line:4');
expect(err.stack).to.contain(message.join('\n'));
expect(err.name).to.equal('RenderError');
});
});
@@ -243,21 +305,22 @@ describe('error', function() {
});
});
it('should contain template content in err.message', function() {
it('should contain template context in err.stack', 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'
' 7| 7th',
'AssertionError: tag a not found',
];
return expect(engine.parseAndRender(html.join('\n'))).to.eventually
.be.rejected
.then(function(err) {
expect(err.message).to.equal(message.join('\n'));
expect(err.message).to.equal('tag a not found, line:4');
expect(err.stack).to.contain(message.join('\n'));
expect(err.name).to.equal('ParseError');
});
});
@@ -265,16 +328,17 @@ describe('error', function() {
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'
' 4| 4th',
'AssertionError: tag a not found',
];
return expect(engine.parseAndRender(html.join('\n'))).to.eventually
.be.rejected
.then(function(err) {
expect(err.message).to.equal(message.join('\n'));
expect(err.message).to.equal('tag a not found, line:2');
expect(err.stack).to.contain(message.join('\n'));
});
});