Making good progress on async

This commit is contained in:
Tim Hardy
2016-09-25 00:13:29 -05:00
parent 87e284159d
commit bb8468708a
8 changed files with 255 additions and 233 deletions
+17 -16
View File
@@ -39,24 +39,25 @@ var _engine = {
return this.renderer.renderTemplates(tpl, scope.factory(ctx)); return this.renderer.renderTemplates(tpl, scope.factory(ctx));
}, },
parseAndRender: function(html, ctx) { parseAndRender: function(html, ctx) {
var tpl = this.parse(html); try {
return this.render(tpl, ctx); var tpl = this.parse(html);
return this.render(tpl, ctx);
}
catch (error) {
// A throw inside of a then or catch of a Promise automatically rejects, but since we mix a sync call
// with an async call, we need to do this in case the sync call throws.
return Promise.reject(error);
}
}, },
renderFile: function(filepath, ctx) { renderFile: function(filepath, ctx) {
try{ return this.handleCache(filepath)
return this.handleCache(filepath) .then((templates) => {
.then((templates) => { return this.render(templates, ctx);
return this.render(templates, ctx); })
}) .catch((e) => {
.catch((e) => { e.file = filepath;
e.file = filepath; throw e;
throw e; });
});
}
catch(e){
e.file = filepath;
throw e;
}
}, },
evalOutput: function(str, scope) { evalOutput: function(str, scope) {
var tpl = this.parser.parseOutput(str.trim()); var tpl = this.parser.parseOutput(str.trim());
+6 -41
View File
@@ -9,41 +9,6 @@ var render = {
assert(scope, 'unable to evalTemplates: scope undefined'); assert(scope, 'unable to evalTemplates: scope undefined');
var html = ''; var html = '';
// var promiseChain = Promise.resolve(''); // create an empty promise to begin the chain;
// templates.some((template, index) => {
// if (scope.get('forloop.skip')) return true;
// var promiseLink = Promise.resolve('');
// switch (template.type) {
// case 'tag':
// // Add Promises to the chain that need to be resolved sequentially
// promiseLink = this.renderTag(template, scope, this.register)
// .then((partial) => {
// if (partial === undefined) return true; // basically a noop (do nothing)
// html += partial;
// });
// promiseChain = promiseChain.then(promiseLink); // add a link to the chain
// break;
// case 'html':
// promiseLink = Promise.resolve(template.value)
// .then((partial) => {
// html += partial;
// });
// promiseChain = promiseChain.then(promiseLink); // add a link to the chain
// break;
// case 'output':
// var val = this.evalOutput(template, scope);
// promiseLink = Promise.resolve(val === undefined ? '' : stringify(val))
// .then((partial) => {
// html += partial;
// });
// promiseChain = promiseChain.then(promiseLink); // add a link to the chain
// }
// });
// return promiseChain.then((result) => {
// // this should happen after all of the above promises are finished, and they should have resolved in order
// return html;
// });
// This executes an array of promises sequentially for every template in the templates array - http://webcache.googleusercontent.com/search?q=cache:rNbMUn9TPtkJ:joost.vunderink.net/blog/2014/12/15/processing-an-array-of-promises-sequentially-in-node-js/+&cd=5&hl=en&ct=clnk&gl=us // This executes an array of promises sequentially for every template in the templates array - http://webcache.googleusercontent.com/search?q=cache:rNbMUn9TPtkJ:joost.vunderink.net/blog/2014/12/15/processing-an-array-of-promises-sequentially-in-node-js/+&cd=5&hl=en&ct=clnk&gl=us
// It's fundamentally equivalent to the following... // It's fundamentally equivalent to the following...
@@ -87,12 +52,12 @@ var render = {
return promiseLink; return promiseLink;
}) })
.catch((error) => { .catch((error) => {
if (error === 'forloop.stop') { if (error.message === 'forloop.skip') {
// the error is a controlled, purposeful stop. so just return the html that we have up to this point // the error is a controlled, purposeful stop. so just return the html that we have up to this point
return html; return html;
} else { } else {
// rethrow actual error // rethrow actual error
throw new Error(error); throw error;
} }
}); });
}, Promise.resolve('')); // start the reduce chain with a resolved Promise. After first run, the "promise" argument }, Promise.resolve('')); // start the reduce chain with a resolved Promise. After first run, the "promise" argument
@@ -100,11 +65,11 @@ var render = {
// case, that's the promise returned from this.renderTag or a resolved promise with raw html. // case, that's the promise returned from this.renderTag or a resolved promise with raw html.
return lastPromise return lastPromise
.then(() => { .then((renderedHtml) => {
return html; return renderedHtml;
}) })
.catch((error) => { .catch((error) => {
throw new Error(error); throw error;
}); });
}, },
@@ -117,7 +82,7 @@ var render = {
if (template.name === 'break') { if (template.name === 'break') {
scope.set('forloop.stop', true); scope.set('forloop.stop', true);
scope.set('forloop.skip', true); scope.set('forloop.skip', true);
return Promise.resolve(''); return Promise.reject(new Error('forloop.stop')); // this will stop the sequential promise chain
} }
return template.render(scope, register); return template.render(scope, register);
}, },
+4 -2
View File
@@ -21,8 +21,10 @@ module.exports = function(liquid) {
stream.start(); stream.start();
}, },
render: function(scope, hash) { render: function(scope, hash) {
var html = liquid.renderer.renderTemplates(this.templates, scope); return liquid.renderer.renderTemplates(this.templates, scope)
scope.set(this.variable, html); .then((html) => {
scope.set(this.variable, html);
});
} }
}); });
+16 -17
View File
@@ -38,9 +38,8 @@ module.exports = function(liquid) {
return liquid.renderer.renderTemplates(this.elseTemplates, scope); return liquid.renderer.renderTemplates(this.elseTemplates, scope);
} }
var html = '', var html = '';
ctx = {}, var length = collection.length;
length = collection.length;
var offset = hash.offset || 0; var offset = hash.offset || 0;
var limit = (hash.limit === undefined) ? collection.length : hash.limit; var limit = (hash.limit === undefined) ? collection.length : hash.limit;
@@ -52,6 +51,7 @@ module.exports = function(liquid) {
// First, we build the array of parameters we are going to use for each call to renderTemplates // First, we build the array of parameters we are going to use for each call to renderTemplates
var contexts = []; var contexts = [];
collection.some((item, i) => { collection.some((item, i) => {
var ctx = {};
ctx[this.variable] = item; ctx[this.variable] = item;
ctx.forloop = { ctx.forloop = {
first: i === 0, first: i === 0,
@@ -78,7 +78,9 @@ module.exports = function(liquid) {
throw new Error('forloop.stop'); // this will stop the sequential promise chain throw new Error('forloop.stop'); // this will stop the sequential promise chain
} }
html += partial; return html += partial;
})
.then((partial) => {
// todo: Make sure our scope management is sound here. Create some tests that revolve around loops // todo: Make sure our scope management is sound here. Create some tests that revolve around loops
// with sections that take differing amounts of time to complete. Make sure the order is maintained // with sections that take differing amounts of time to complete. Make sure the order is maintained
// and scope doesn't bleed over into other renderTemplate calls. // and scope doesn't bleed over into other renderTemplate calls.
@@ -88,26 +90,23 @@ module.exports = function(liquid) {
.then((partial) => { .then((partial) => {
scope.pop(context); scope.pop(context);
return partial; return partial;
})
.catch((error) => {
if (error === 'forloop.stop') {
// the error is a controlled, purposeful stop. so just return the html that we have up to this point
return html;
} else {
// rethrow actual error
throw new Error(error);
}
}); });
}, Promise.resolve()); // start the reduce chain with a resolved Promise. After first run, the "promise" argument }, Promise.resolve('')); // start the reduce chain with a resolved Promise. After first run, the "promise" argument
// in our reduce callback will be the returned promise from our "then" above. In this // in our reduce callback will be the returned promise from our "then" above. In this
// case, the promise returned from liquid.renderer.renderTemplates. // case, the promise returned from liquid.renderer.renderTemplates.
return lastPromise return lastPromise
.then(() => { .then((partial) => {
return html; return html += partial;
}) })
.catch((error) => { .catch((error) => {
throw new Error(error); if (error.message === 'forloop.stop') {
// the error is a controlled, purposeful stop. so just return the html that we have up to this point
return html;
} else {
// rethrow actual error
throw error;
}
}); });
} }
+45 -9
View File
@@ -1,4 +1,5 @@
var Liquid = require('..'); var Liquid = require('..');
var Promise = require('any-promise');
var lexical = Liquid.lexical; var lexical = Liquid.lexical;
var withRE = new RegExp(`with\\s+(${lexical.value.source})`); var withRE = new RegExp(`with\\s+(${lexical.value.source})`);
@@ -14,13 +15,34 @@ module.exports = function(liquid) {
}, },
render: function(scope, hash) { render: function(scope, hash) {
var layout = Liquid.evalValue(this.layout, scope); var layout = Liquid.evalValue(this.layout, scope);
var tpl = liquid.handleCache(layout);
var html = '';
scope.push({}); scope.push({});
liquid.renderer.renderTemplates(this.tpls, scope); // not sure if this first one is needed, since the results are ignored
var html = liquid.renderer.renderTemplates(tpl, scope); return liquid.renderer.renderTemplates(this.tpls, scope)
scope.pop(); .then((partial) => {
return html; html += partial;
return liquid.handleCache(layout)
})
.then((templates) => {
return liquid.renderer.renderTemplates(templates, scope);
})
.then((partial) => {
scope.pop();
return partial;
})
.catch((e) => {
e.file = layout;
throw e;
});
// var tpl = liquid.handleCache(layout);
//
// scope.push({});
// liquid.renderer.renderTemplates(this.tpls, scope); // what's the point of this line?
// var html = liquid.renderer.renderTemplates(tpl, scope);
// scope.pop();
// return html;
} }
}); });
@@ -40,11 +62,25 @@ module.exports = function(liquid) {
}, },
render: function(scope, hash){ render: function(scope, hash){
var html = scope.get(`_liquid.blocks.${this.block}`); var html = scope.get(`_liquid.blocks.${this.block}`);
if(html === undefined){ var promise = Promise.resolve('');
html = liquid.renderer.renderTemplates(this.tpls, scope); if (html === undefined) {
promise = liquid.renderer.renderTemplates(this.tpls, scope)
.then((partial) => {
scope.set(`_liquid.blocks.${this.block}`, partial);
return partial;
});
} }
scope.set(`_liquid.blocks.${this.block}`, html); else {
return html; scope.set(`_liquid.blocks.${this.block}`, html);
promise = Promise.resolve(html);
}
return promise;
// if(html === undefined){
// html = liquid.renderer.renderTemplates(this.tpls, scope);
// }
// scope.set(`_liquid.blocks.${this.block}`, html);
// return html;
} }
}); });
+25 -17
View File
@@ -1,4 +1,5 @@
var Liquid = require('..'); var Liquid = require('..');
var Promise = require('any-promise');
var lexical = Liquid.lexical; var lexical = Liquid.lexical;
var re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+` + var re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+` +
`(${lexical.value.source})` + `(${lexical.value.source})` +
@@ -31,7 +32,6 @@ module.exports = function(liquid) {
var html = '<table>', var html = '<table>',
promiseChain = Promise.resolve(''); // create an empty promise to begin the chain promiseChain = Promise.resolve(''); // create an empty promise to begin the chain
ctx = {},
length = collection.length; length = collection.length;
var offset = hash.offset || 0; var offset = hash.offset || 0;
var limit = (hash.limit === undefined) ? collection.length : hash.limit; var limit = (hash.limit === undefined) ? collection.length : hash.limit;
@@ -40,7 +40,14 @@ module.exports = function(liquid) {
if (!cols) throw new Error(`illegal cols: ${cols}`); if (!cols) throw new Error(`illegal cols: ${cols}`);
// build array of arguments to pass to sequential promises... // build array of arguments to pass to sequential promises...
var contexts = collection.slice(offset, offset + limit); collection = collection.slice(offset, offset + limit);
var contexts = [];
collection.some((item, i) => {
var ctx = {};
ctx[this.variable] = item;
// We are just putting together an array of the arguments we will be passing to our sequential promises
contexts.push(ctx);
});
// This executes an array of promises sequentially for every argument in the contexts array - http://webcache.googleusercontent.com/search?q=cache:rNbMUn9TPtkJ:joost.vunderink.net/blog/2014/12/15/processing-an-array-of-promises-sequentially-in-node-js/+&cd=5&hl=en&ct=clnk&gl=us // This executes an array of promises sequentially for every argument in the contexts array - http://webcache.googleusercontent.com/search?q=cache:rNbMUn9TPtkJ:joost.vunderink.net/blog/2014/12/15/processing-an-array-of-promises-sequentially-in-node-js/+&cd=5&hl=en&ct=clnk&gl=us
// It's fundamentally equivalent to the following... // It's fundamentally equivalent to the following...
@@ -56,32 +63,33 @@ module.exports = function(liquid) {
html += `<tr class="row${row}">`; html += `<tr class="row${row}">`;
} }
ctx[this.variable] = context; //ctx[this.variable] = context;
scope.push(ctx);
html += `<td class="col${col}">`; return html += `<td class="col${col}">`;
})
.then((partial) => {
scope.push(context);
return liquid.renderer.renderTemplates(this.templates, scope) return liquid.renderer.renderTemplates(this.templates, scope)
}) })
.then((partial) => { .then((partial) => {
html += partial;
html += '</td>';
scope.pop(context); scope.pop(context);
return partial; // I think this is currently unused (partial is not used in the above "then") html += partial;
}) return html += '</td>';
.catch((error) => {
throw new Error(error);
}); });
}, Promise.resolve()); // start the reduce chain with a resolved Promise. After first run, the "promise" argument }, Promise.resolve('')); // start the reduce chain with a resolved Promise. After first run, the "promise" argument
// in our reduce callback will be the returned promise from our "then" above. In this // in our reduce callback will be the returned promise from our "then" above. In this
// case, the promise returned from liquid.renderer.renderTemplates. // case, the promise returned from liquid.renderer.renderTemplates.
lastPromise return lastPromise
.then(() => { .then(() => {
if(row > 0) html += '</tr>'; if(row > 0) {
html += '</tr>';
}
html += '</table>'; html += '</table>';
return html; return html;
}) })
.catch((error) => { .catch((error) => {
throw new Error(error); throw error;
}); });
} }
}); });
+50 -30
View File
@@ -1,8 +1,10 @@
const chai = require("chai"); const chai = require("chai");
const chaiAsPromised = require("chai-as-promised");
const should = chai.should();
const expect = chai.expect; const expect = chai.expect;
const should = chai.should;
const Liquid = require('..'); const Liquid = require('..');
const mock = require('mock-fs'); const mock = require('mock-fs');
chai.use(chaiAsPromised);
describe('liquid', function() { describe('liquid', function() {
var engine, ctx; var engine, ctx;
@@ -27,13 +29,13 @@ describe('liquid', function() {
mock.restore(); mock.restore();
}); });
it('should output object', function() { it('should output object', function() {
engine.parseAndRender('{{obj}}', ctx).should.equal('{"foo":"bar"}'); return engine.parseAndRender('{{obj}}', ctx).should.eventually.equal('{"foo":"bar"}');
}); });
it('should output array', function() { it('should output array', function() {
engine.parseAndRender('{{arr}}', ctx).should.equal('[-2,"a"]'); return engine.parseAndRender('{{arr}}', ctx).should.eventually.equal('[-2,"a"]');
}); });
it('should output undefined to empty', function() { it('should output undefined to empty', function() {
engine.parseAndRender('foo{{zzz}}bar', ctx).should.equal('foobar'); return engine.parseAndRender('foo{{zzz}}bar', ctx).should.eventually.equal('foobar');
}); });
it('should parse html', function() { it('should parse html', function() {
(function() { (function() {
@@ -45,46 +47,55 @@ describe('liquid', function() {
}); });
it('should render template multiple times', function() { it('should render template multiple times', function() {
var template = engine.parse('{{obj}}'); var template = engine.parse('{{obj}}');
engine.render(template, ctx).should.equal('{"foo":"bar"}'); return engine.render(template, ctx)
engine.render(template, ctx).should.equal('{"foo":"bar"}'); .then((result) => {
expect(result).to.equal('{"foo":"bar"}');
return engine.render(template, ctx);
})
.then((result) => {
return expect(result).to.equal('{"foo":"bar"}');
});
// engine.render(template, ctx).should.equal('{"foo":"bar"}');
// engine.render(template, ctx).should.equal('{"foo":"bar"}');
}); });
it('should render filters', function() { it('should render filters', function() {
var template = engine.parse('<p>{{arr | join: "_"}}</p>'); var template = engine.parse('<p>{{arr | join: "_"}}</p>');
engine.render(template, ctx).should.equal('<p>-2_a</p>'); return engine.render(template, ctx).should.eventually.equal('<p>-2_a</p>');
}); });
describe('#renderFile()', function(){ describe('#renderFile()', function(){
it('should render file', function() { it('should render file', function() {
engine.renderFile('/root/files/foo.html', ctx).should.equal('foo'); return engine.renderFile('/root/files/foo.html', ctx).should.eventually.equal('foo');
}); });
it('should render file relative to root', function() { it('should render file relative to root', function() {
engine.renderFile('files/foo.html', ctx).should.equal('foo'); return engine.renderFile('files/foo.html', ctx).should.eventually.equal('foo');
}); });
it('should render file with context', function() { it('should render file with context', function() {
engine.renderFile('/root/files/name.html', ctx).should.equal('My name is harttle.'); return engine.renderFile('/root/files/name.html', ctx).should.eventually.equal('My name is harttle.');
}); });
it('should render file with default extname', function() { it('should render file with default extname', function() {
engine.renderFile('files/name', ctx).should.equal('My name is harttle.'); return engine.renderFile('files/name', ctx).should.eventually.equal('My name is harttle.');
});
});
describe('#express()', function() {
it('should render templates', function() {
engine.express()('/root/files/name.html', ctx, function(err, html) {
expect(err).to.equal(null);
expect(html).to.equal('My name is harttle.');
});
});
it('should pass error when file not found', function() {
engine.express()('/root/files/name1.html', ctx, function(err, html) {
expect(err.code).to.equal('ENOENT');
});
}); });
}); });
// todo: make these async
// describe('#express()', function() {
// it('should render templates', function() {
// engine.express()('/root/files/name.html', ctx, function(err, html) {
// expect(err).to.equal(null);
// expect(html).to.equal('My name is harttle.');
// });
// });
// it('should pass error when file not found', function() {
// engine.express()('/root/files/name1.html', ctx, function(err, html) {
// expect(err.code).to.equal('ENOENT');
// });
// });
// });
describe('cache', function() { describe('cache', function() {
it('should be disabled by default', function() { it('should be disabled by default', function() {
mock({ mock({
'/root/files/foo.html': 'bar' '/root/files/foo.html': 'bar'
}); });
engine.renderFile('files/foo', ctx).should.equal('bar'); return engine.renderFile('files/foo', ctx).should.eventually.equal('bar');
}); });
it('should respect cache=true option', function() { it('should respect cache=true option', function() {
engine = Liquid({ engine = Liquid({
@@ -92,11 +103,20 @@ describe('liquid', function() {
extname: '.html', extname: '.html',
cache: true cache: true
}); });
engine.renderFile('files/foo', ctx).should.equal('foo'); return engine.renderFile('files/foo', ctx)
mock({ .then((result) => {
'/root/files/foo.html': 'bar' return expect(result).to.equal('foo');
}); })
engine.renderFile('files/foo', ctx).should.equal('foo'); .then((result) => {
mock({
'/root/files/foo.html': 'bar'
});
return engine.renderFile('files/foo', ctx);
})
.then((result) => {
return expect(result).to.equal('foo');
});
}); });
}); });
}); });
+92 -101
View File
@@ -1,11 +1,14 @@
// temporary // temporary
var Promise = require('any-promise'); const Promise = require('any-promise');
const chai = require("chai"); const chai = require("chai");
const chaiAsPromised = require("chai-as-promised");
const should = chai.should(); const should = chai.should();
const expect = chai.expect; const expect = chai.expect;
const Liquid = require('..'); const Liquid = require('..');
const mock = require('mock-fs'); const mock = require('mock-fs');
chai.use(require("chai-as-promised"));
chai.use(chaiAsPromised);
var liquid = Liquid({ var liquid = Liquid({
root: '/', root: '/',
extname: '.html' extname: '.html'
@@ -13,17 +16,14 @@ var liquid = Liquid({
ctx, src, dst; ctx, src, dst;
function test(src, dst) { function test(src, dst) {
liquid.parseAndRender(src, ctx) return liquid.parseAndRender(src, ctx)
.then((result) => { .then((result) => {
expect(result.to.equal(dst)); return expect(result).to.equal(dst);
}); });
//expect(liquid.parseAndRender(src, ctx)).to.equal(dst);
} }
function testThrow(src, pattern) { function testThrow(src, pattern) {
expect(function() { return liquid.parseAndRender(src, ctx).should.eventually.be.rejectedWith(pattern);
liquid.parseAndRender(src, ctx);
}).to.throw(pattern);
} }
describe('tags', function() { describe('tags', function() {
@@ -67,76 +67,63 @@ describe('tags', function() {
mock.restore(); mock.restore();
}); });
it('should support assign', function() { it('should support assign 1', function() { return test('{% assign foo="bar" %}{{foo}}', 'bar'); });
test('{% assign foo="bar" %}{{foo}}', 'bar'); it('should support assign 2', function() { return test('{% assign foo=(1..3) %}{{foo}}', '[1,2,3]'); });
test('{% assign foo=(1..3) %}{{foo}}', '[1,2,3]'); it('should support assign 3', function() { return test('{% assign foo="a b" | capitalize | split: " " | first %}{{foo}}', 'A'); });
test('{% assign foo="a b" | capitalize | split: " " | first %}{{foo}}', 'A');
});
it('should support raw', function() { it('should support raw 1', function() { return testThrow('{% raw%}', /{% raw%} not closed/); });
testThrow('{% raw%}', /{% raw%} not closed/); it('should support raw 2', function() { return test('{% raw %}{{ 5 | plus: 6 }}{% endraw %} is equal to 11.', '{{ 5 | plus: 6 }} is equal to 11.'); });
test('{% raw %}{{ 5 | plus: 6 }}{% endraw %} is equal to 11.', '{{ 5 | plus: 6 }} is equal to 11.'); it('should support raw 3', function() { return test('{% raw %}\n{{ foo}} \n{% endraw %}', '\n{{ foo}} \n'); });
test('{% raw %}\n{{ foo}} \n{% endraw %}', '\n{{ foo}} \n');
});
it('should support comment', function() {
testThrow('{% comment %}{% raw%}', /{% comment %} not closed/);
test('My name is {% comment %}super{% endcomment %} Shopify.', 'My name is Shopify.');
test('{% comment %}\n{{ foo}} \n{% endcomment %}', '');
});
it('should support case', function() { it('should support comment 1', function() { return testThrow('{% comment %}{% raw%}', /{% comment %} not closed/); });
testThrow('{% case "foo"%}', /{% case "foo"%} not closed/); it('should support comment 2', function() { return test('My name is {% comment %}super{% endcomment %} Shopify.', 'My name is Shopify.'); });
test('{% case "foo"%}' + it('should support comment 3', function() { return test('{% comment %}\n{{ foo}} \n{% endcomment %}', ''); });
'{% when "foo" %}foo{% when "bar"%}bar' +
'{%endcase%}', 'foo');
test('{% case empty %}' +
'{% when "foo" %}foo{% when ""%}bar' +
'{%endcase%}', 'bar');
test('{% case false %}' +
'{% when "foo" %}foo{% when ""%}bar' +
'{%endcase%}', '');
test('{% case "a" %}' +
'{% when "b" %}b{% when "c"%}c{%else %}d' +
'{%endcase%}', 'd');
});
it('should support if', function() { it('should support case 1', function() { return testThrow('{% case "foo"%}', /{% case "foo"%} not closed/); });
testThrow('{% if false%}yes', /tag {% if false%} not closed/); it('should support case 2', function() { return test('{% case "foo"%}' +
test('{%if emptyArray%}a{%endif%}', ''); '{% when "foo" %}foo{% when "bar"%}bar' +
test('{% if 2==3 %}yes{%else%}no{%endif%}', 'no'); '{%endcase%}', 'foo'); });
test('{% if 1>=2 and one<two %}a{%endif%}', ''); it('should support case 3', function() { return test('{% case empty %}' +
test('{% if one!=two %}yes{%else%}no{%endif%}', 'yes'); '{% when "foo" %}foo{% when ""%}bar' +
test('{% if false %}1{%elsif true%}2{%else%}3{%endif%}', '2'); '{%endcase%}', 'bar'); });
test('{%if false%}{%if true%}{%else%}a{%endif%}{%endif%}', ''); it('should support case 4', function() { return test('{% case false %}' +
}); '{% when "foo" %}foo{% when ""%}bar' +
'{%endcase%}', ''); });
it('should support case 5', function() { return test('{% case "a" %}' +
'{% when "b" %}b{% when "c"%}c{%else %}d' +
'{%endcase%}', 'd'); });
it('should support unless', function() { it('should support if 1', function() { return testThrow('{% if false%}yes', /tag {% if false%} not closed/); });
test('{% unless 1 %}yes{%else%}no{%endunless%}', 'no'); it('should support if 2', function() { return test('{%if emptyArray%}a{%endif%}', ''); });
testThrow('{% unless 1>2 %}yes', /tag {% unless 1>2 %} not closed/); it('should support if 3', function() { return test('{% if 2==3 %}yes{%else%}no{%endif%}', 'no'); });
test('{% unless 1>2 %}yes{%endunless%}', 'yes'); it('should support if 4', function() { return test('{% if 1>=2 and one<two %}a{%endif%}', ''); });
test('{% unless true %}{%endunless%}', ''); it('should support if 5', function() { return test('{% if one!=two %}yes{%else%}no{%endif%}', 'yes'); });
}); it('should support if 6', function() { return test('{% if false %}1{%elsif true%}2{%else%}3{%endif%}', '2'); });
it('should support if 7', function() { return test('{%if false%}{%if true%}{%else%}a{%endif%}{%endif%}', ''); });
it('should support capture', function() { it('should support unless 1', function() { return test('{% unless 1 %}yes{%else%}no{%endunless%}', 'no'); });
test('{% capture f %}{{"a" | capitalize}}{%endcapture%}{{f}}', 'A'); it('should support unless 2', function() { return testThrow('{% unless 1>2 %}yes', /tag {% unless 1>2 %} not closed/); });
testThrow('{% capture = %}{%endcapture%}', /= not valid identifier/); it('should support unless 3', function() { return test('{% unless 1>2 %}yes{%endunless%}', 'yes'); });
}); it('should support unless 4', function() { return test('{% unless true %}{%endunless%}', ''); });
it('should support capture 1', function() { return test('{% capture f %}{{"a" | capitalize}}{%endcapture%}{{f}}', 'A'); });
it('should support capture 2', function() { return testThrow('{% capture = %}{%endcapture%}', /= not valid identifier/); });
it('should throw when for capture closed', function() { it('should throw when for capture closed', function() {
testThrow('{%capture c%}{{c}}', /tag .* not closed/); return testThrow('{%capture c%}{{c}}', /tag .* not closed/);
}); });
it('should support for', function() { it('should support for', function() {
test('{%for c in alpha%}{{c}}{%endfor%}', 'abc'); return test('{%for c in alpha%}{{c}}{%endfor%}', 'abc');
}); });
it('should throw when for not closed', function() { it('should throw when for not closed', function() {
testThrow('{%for c in alpha%}{{c}}', /tag .* not closed/); return testThrow('{%for c in alpha%}{{c}}', /tag .* not closed/);
}); });
it('should support for else', function() { it('should support for else', function() {
test('{%for c in ""%}a{%else%}b{%endfor%}', 'b'); return test('{%for c in ""%}a{%else%}b{%endfor%}', 'b');
}); });
it('should support for with forloop', function() { it('should support for with forloop', function() {
@@ -149,65 +136,65 @@ describe('tags', function() {
dst = 'true.1.0.false.3.3.2a\n' + dst = 'true.1.0.false.3.3.2a\n' +
'false.2.1.false.3.2.1b\n' + 'false.2.1.false.3.2.1b\n' +
'false.3.2.true.3.1.0c\n'; 'false.3.2.true.3.1.0c\n';
test(src, dst); return test(src, dst);
}); });
it('should support for with continue and break', function() { it('should support for with continue', function() {
src = '{% for i in (1..5) %}' + src = '{% for i in (1..5) %}' +
'{% if i == 4 %}{% continue %}' + '{% if i == 4 %}{% continue %}' +
'{% else %}{{ i }}' + '{% else %}{{ i }}' +
'{% endif %}' + '{% endif %}' +
'{% endfor %}'; '{% endfor %}';
test(src, '1235'); return test(src, '1235');
});
it('should support for with break', function() {
src = '{% for i in (one..5) %}' + src = '{% for i in (one..5) %}' +
'{% if i == 4 %}{% break %}{% endif %}' + '{% if i == 4 %}{% break %}{% endif %}' +
'{{ i }}' + '{{ i }}' +
'{% endfor %}'; '{% endfor %}';
test(src, '123'); return test(src, '123');
}); });
it('should support for with limit and offset', function() { it('should support for with limit', function() {
src = '{% for i in (1..5) limit:2 %}{{ i }}{% endfor %}'; src = '{% for i in (1..5) limit:2 %}{{ i }}{% endfor %}';
test(src, '12'); return test(src, '12');
});
it('should support for with limit and offset', function() {
src = '{% for i in (1..10) limit:2 offset:5%}{{ i }}{% endfor %}'; src = '{% for i in (1..10) limit:2 offset:5%}{{ i }}{% endfor %}';
test(src, '67'); return test(src, '67');
}); });
it('should support for reversed', function() { it('should support for reversed', function() {
src = '{% for i in (1..5) limit:2 reversed %}{{ i }}{% endfor %}'; src = '{% for i in (1..5) limit:2 reversed %}{{ i }}{% endfor %}';
test(src, '21'); return test(src, '21');
}); });
it('should support cycle', function() { it('should support cycle', function() {
src = "{% cycle '1', '2', '3' %}"; src = "{% cycle '1', '2', '3' %}";
test(src + src + src + src, '1231'); return test(src + src + src + src, '1231');
}); });
it('should throw when cycle candidates empty', function() { it('should throw when cycle candidates empty', function() {
testThrow('{%cycle%}', /empty candidates/); return testThrow('{%cycle%}', /empty candidates/);
}); });
it('should support cycle in for block', function() { it('should support cycle in for block', function() {
src = '{% for i in (1..5) %}{% cycle one, "e"%}{% endfor %}'; src = '{% for i in (1..5) %}{% cycle one, "e"%}{% endfor %}';
test(src, '1e1e1'); return test(src, '1e1e1');
}); });
it('should support cycle group', function() { it('should support cycle group', function() {
src = "{% cycle one: '1', '2', '3'%}" + src = "{% cycle one: '1', '2', '3'%}" +
"{% cycle 1: '1', '2', '3'%}" + "{% cycle 1: '1', '2', '3'%}" +
"{% cycle 2: '1', '2', '3'%}"; "{% cycle 2: '1', '2', '3'%}";
test(src, '121'); return test(src, '121');
}); });
it('should support increment', function() { it('should support increment 1', function() { return test('{% increment foo %}{%increment foo%}{{foo}}', '2'); });
test('{% increment foo %}{%increment foo%}{{foo}}', '2'); it('should support increment 2', function() { return test('{% increment one %}{{one}}', '2'); });
test('{% increment one %}{{one}}', '2');
});
it('should support decrement', function() { it('should support decrement 1', function() { return test('{% decrement foo %}{%decrement foo%}{{foo}}', '-2'); });
test('{% decrement foo %}{%decrement foo%}{{foo}}', '-2'); it('should support decrement 2', function() { return test('{% decrement one %}{{one}}', '0'); });
test('{% decrement one %}{{one}}', '0');
});
it('should support tablerow', function() { it('should support tablerow', function() {
src = '{% tablerow i in alpha cols:2 %}{{ i }}{% endtablerow %}'; src = '{% tablerow i in alpha cols:2 %}{{ i }}{% endtablerow %}';
@@ -215,18 +202,18 @@ describe('tags', function() {
'<tr class="row1"><td class="col1">a</td><td class="col2">b</td></tr>' + '<tr class="row1"><td class="col1">a</td><td class="col2">b</td></tr>' +
'<tr class="row2"><td class="col1">c</td></tr>' + '<tr class="row2"><td class="col1">c</td></tr>' +
'</table>'; '</table>';
test(src, dst); return test(src, dst);
}); });
it('should support empty tablerow', function() { it('should support empty tablerow', function() {
src = '{% tablerow i in (1..0) cols:2 %}{{ i }}{% endtablerow %}'; src = '{% tablerow i in (1..0) cols:2 %}{{ i }}{% endtablerow %}';
dst = '<table></table>'; dst = '<table></table>';
test(src, dst); return test(src, dst);
}); });
it('should throw when tablerow not closed', function() { it('should throw when tablerow not closed', function() {
src = '{% tablerow i in (1..0) cols:2 %}{{ i }}'; src = '{% tablerow i in (1..0) cols:2 %}{{ i }}';
testThrow(src, /tag .* not closed/); return testThrow(src, /tag .* not closed/);
}); });
it('should support tablerow with range', function() { it('should support tablerow with range', function() {
@@ -236,13 +223,15 @@ describe('tags', function() {
'<tr class="row2"><td class="col1">3</td><td class="col2">4</td></tr>' + '<tr class="row2"><td class="col1">3</td><td class="col2">4</td></tr>' +
'<tr class="row3"><td class="col1">5</td></tr>' + '<tr class="row3"><td class="col1">5</td></tr>' +
'</table>'; '</table>';
test(src, dst); return test(src, dst);
}); });
it('tablerow should throw on illegal cols', function() { it('tablerow should throw on illegal cols 1', function() {
testThrow('{% tablerow i in (1..5) cols:0 %}{{ i }}{% endtablerow %}', return testThrow('{% tablerow i in (1..5) cols:0 %}{{ i }}{% endtablerow %}',
/illegal cols: 0/); /illegal cols: 0/);
testThrow('{% tablerow i in (1..5) %}{{ i }}{% endtablerow %}', });
it('tablerow should throw on illegal cols 2', function() {
return testThrow('{% tablerow i in (1..5) %}{{ i }}{% endtablerow %}',
/illegal cols: undefined/); /illegal cols: undefined/);
}); });
@@ -252,7 +241,7 @@ describe('tags', function() {
'<tr class="row1"><td class="col1">1</td><td class="col2">2</td></tr>' + '<tr class="row1"><td class="col1">1</td><td class="col2">2</td></tr>' +
'<tr class="row2"><td class="col1">3</td></tr>' + '<tr class="row2"><td class="col1">3</td></tr>' +
'</table>'; '</table>';
test(src, dst); return test(src, dst);
}); });
it('should support tablerow with offset', function() { it('should support tablerow with offset', function() {
@@ -260,29 +249,29 @@ describe('tags', function() {
dst = '<table>' + dst = '<table>' +
'<tr class="row1"><td class="col1">4</td><td class="col2">5</td></tr>' + '<tr class="row1"><td class="col1">4</td><td class="col2">5</td></tr>' +
'</table>'; '</table>';
test(src, dst); return test(src, dst);
}); });
it.only('should support include', function() { it('should support include', function() {
return liquid.renderFile('/current.html', ctx).should.eventually.equal('barFOObar'); return liquid.renderFile('/current.html', ctx).should.eventually.equal('barFOObar');
}); });
it('should support include with relative path', function() { it('should support include with relative path', function() {
expect(liquid.renderFile('relative.html', ctx)).to.equal('barfoobar'); return liquid.renderFile('relative.html', ctx).should.eventually.equal('barfoobar');
}); });
it('should support include: hash list', function() { it('should support include: hash list', function() {
expect(liquid.renderFile('hash.html', ctx)).to.equal('harttle : admin : harttle'); return liquid.renderFile('hash.html', ctx).should.eventually.equal('harttle : admin : harttle');
}); });
it('should support include: parent scope', function() { it('should support include: parent scope', function() {
expect(liquid.renderFile('scope.html', ctx)).to.equal('color:yellow, shape:triangle'); return liquid.renderFile('scope.html', ctx).should.eventually.equal('color:yellow, shape:triangle');
}); });
it('should support include: with', function() { it('should support include: with', function() {
var filepath = 'with.html'; var filepath = 'with.html';
var dst = 'color:red, shape:rect'; var dst = 'color:red, shape:rect';
expect(liquid.renderFile(filepath, ctx)).to.equal(dst); return liquid.renderFile(filepath, ctx).should.eventually.equal(dst);
}); });
it('should support nested includes', function() { it('should support nested includes', function() {
@@ -292,22 +281,24 @@ describe('tags', function() {
it('should throw when block not closed', function() { it('should throw when block not closed', function() {
src = '{% layout "default-layout" %}{%block%}bar'; src = '{% layout "default-layout" %}{%block%}bar';
testThrow(src, /tag {%block%} not closed/); return testThrow(src, /tag {%block%} not closed/);
}); });
it('should support layout', function() { it('should support layout', function() {
src = '{% layout "default-layout" %}{%block%}bar{%endblock%}'; src = '{% layout "default-layout" %}{%block%}bar{%endblock%}';
test(src, 'foobarfoo'); return test(src, 'foobarfoo');
}); });
it('should support layout: multiple blocks', function() { it('should support layout: multiple blocks', function() {
src = '{% layout "multi-blocks-layout" %}' + src = '{% layout "multi-blocks-layout" %}' +
'{%block a%}bara{%endblock%}' + '{%block a%}bara{%endblock%}' +
'{%block b%}barb{%endblock%}'; '{%block b%}barb{%endblock%}';
test(src, 'foobarabarbfoo'); return test(src, 'foobarabarbfoo');
}); });
it('should support layout: nested', function() { it('should support layout: nested 1', function() {
src = '{% layout "multi-blocks" %}{% block a%}A{%endblock%}{%block c%}C{%endblock%}'; src = '{% layout "multi-blocks" %}{% block a%}A{%endblock%}{%block c%}C{%endblock%}';
test(src, 'fooA;C;foo'); return test(src, 'fooA;C;foo');
});
it('should support layout: nested 2', function() {
src = '{% layout "multi-blocks" %}{%block c%}C{%endblock%}'; src = '{% layout "multi-blocks" %}{%block c%}C{%endblock%}';
test(src, 'fooaaa;C;foo'); return test(src, 'fooaaa;C;foo');
}); });
}); });