diff --git a/index.js b/index.js index 944d151fd..ad0fceebf 100644 --- a/index.js +++ b/index.js @@ -39,24 +39,25 @@ var _engine = { return this.renderer.renderTemplates(tpl, scope.factory(ctx)); }, parseAndRender: function(html, ctx) { - var tpl = this.parse(html); - return this.render(tpl, ctx); + try { + 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) { - try{ - return this.handleCache(filepath) - .then((templates) => { - return this.render(templates, ctx); - }) - .catch((e) => { - e.file = filepath; - throw e; - }); - } - catch(e){ - e.file = filepath; - throw e; - } + return this.handleCache(filepath) + .then((templates) => { + return this.render(templates, ctx); + }) + .catch((e) => { + e.file = filepath; + throw e; + }); }, evalOutput: function(str, scope) { var tpl = this.parser.parseOutput(str.trim()); diff --git a/src/render.js b/src/render.js index 9e16e65c1..7812c29fe 100644 --- a/src/render.js +++ b/src/render.js @@ -9,41 +9,6 @@ var render = { assert(scope, 'unable to evalTemplates: scope undefined'); 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 // It's fundamentally equivalent to the following... @@ -87,12 +52,12 @@ var render = { return promiseLink; }) .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 return html; } else { // 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 @@ -100,11 +65,11 @@ var render = { // case, that's the promise returned from this.renderTag or a resolved promise with raw html. return lastPromise - .then(() => { - return html; + .then((renderedHtml) => { + return renderedHtml; }) .catch((error) => { - throw new Error(error); + throw error; }); }, @@ -117,7 +82,7 @@ var render = { if (template.name === 'break') { scope.set('forloop.stop', 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); }, diff --git a/tags/capture.js b/tags/capture.js index ff6af14ba..6a315887f 100644 --- a/tags/capture.js +++ b/tags/capture.js @@ -21,8 +21,10 @@ module.exports = function(liquid) { stream.start(); }, render: function(scope, hash) { - var html = liquid.renderer.renderTemplates(this.templates, scope); - scope.set(this.variable, html); + return liquid.renderer.renderTemplates(this.templates, scope) + .then((html) => { + scope.set(this.variable, html); + }); } }); diff --git a/tags/for.js b/tags/for.js index b69773965..7dec260de 100644 --- a/tags/for.js +++ b/tags/for.js @@ -38,9 +38,8 @@ module.exports = function(liquid) { return liquid.renderer.renderTemplates(this.elseTemplates, scope); } - var html = '', - ctx = {}, - length = collection.length; + var html = ''; + var length = collection.length; var offset = hash.offset || 0; 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 var contexts = []; collection.some((item, i) => { + var ctx = {}; ctx[this.variable] = item; ctx.forloop = { first: i === 0, @@ -78,7 +78,9 @@ module.exports = function(liquid) { 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 // 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. @@ -88,26 +90,23 @@ module.exports = function(liquid) { .then((partial) => { scope.pop(context); 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 // case, the promise returned from liquid.renderer.renderTemplates. return lastPromise - .then(() => { - return html; + .then((partial) => { + return html += partial; }) .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; + } }); } diff --git a/tags/layout.js b/tags/layout.js index bd776c9ce..bf2365908 100644 --- a/tags/layout.js +++ b/tags/layout.js @@ -1,4 +1,5 @@ var Liquid = require('..'); +var Promise = require('any-promise'); var lexical = Liquid.lexical; var withRE = new RegExp(`with\\s+(${lexical.value.source})`); @@ -14,13 +15,34 @@ module.exports = function(liquid) { }, render: function(scope, hash) { var layout = Liquid.evalValue(this.layout, scope); - var tpl = liquid.handleCache(layout); + var html = ''; scope.push({}); - liquid.renderer.renderTemplates(this.tpls, scope); - var html = liquid.renderer.renderTemplates(tpl, scope); - scope.pop(); - return html; + // not sure if this first one is needed, since the results are ignored + return liquid.renderer.renderTemplates(this.tpls, scope) + .then((partial) => { + 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){ var html = scope.get(`_liquid.blocks.${this.block}`); - if(html === undefined){ - html = liquid.renderer.renderTemplates(this.tpls, scope); + var promise = Promise.resolve(''); + 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); - return html; + else { + 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; } }); diff --git a/tags/tablerow.js b/tags/tablerow.js index ecf24cdaf..07077a460 100644 --- a/tags/tablerow.js +++ b/tags/tablerow.js @@ -1,4 +1,5 @@ var Liquid = require('..'); +var Promise = require('any-promise'); var lexical = Liquid.lexical; var re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+` + `(${lexical.value.source})` + @@ -31,7 +32,6 @@ module.exports = function(liquid) { var html = '
| `; + //ctx[this.variable] = context; + + return html += ` | `; + }) + .then((partial) => { + scope.push(context); return liquid.renderer.renderTemplates(this.templates, scope) }) .then((partial) => { - html += partial; - html += ' | '; scope.pop(context); - return partial; // I think this is currently unused (partial is not used in the above "then") - }) - .catch((error) => { - throw new Error(error); + html += partial; + return html += ''; }); - }, 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 - // case, the promise returned from liquid.renderer.renderTemplates. + }, 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 + // case, the promise returned from liquid.renderer.renderTemplates. - lastPromise + return lastPromise .then(() => { - if(row > 0) html += '
{{arr | join: "_"}}
'); - engine.render(template, ctx).should.equal('-2_a
'); + return engine.render(template, ctx).should.eventually.equal('-2_a
'); }); describe('#renderFile()', 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() { - 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() { - 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() { - engine.renderFile('files/name', ctx).should.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'); - }); + return engine.renderFile('files/name', ctx).should.eventually.equal('My name is harttle.'); }); }); + // 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() { it('should be disabled by default', function() { mock({ '/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() { engine = Liquid({ @@ -92,11 +103,20 @@ describe('liquid', function() { extname: '.html', cache: true }); - engine.renderFile('files/foo', ctx).should.equal('foo'); - mock({ - '/root/files/foo.html': 'bar' - }); - engine.renderFile('files/foo', ctx).should.equal('foo'); + return engine.renderFile('files/foo', ctx) + .then((result) => { + return expect(result).to.equal('foo'); + }) + .then((result) => { + mock({ + '/root/files/foo.html': 'bar' + }); + return engine.renderFile('files/foo', ctx); + }) + .then((result) => { + return expect(result).to.equal('foo'); + }); + }); }); }); diff --git a/test/tags.js b/test/tags.js index 35df7ce9e..7df2f1a85 100644 --- a/test/tags.js +++ b/test/tags.js @@ -1,11 +1,14 @@ // temporary -var Promise = require('any-promise'); +const Promise = require('any-promise'); const chai = require("chai"); +const chaiAsPromised = require("chai-as-promised"); const should = chai.should(); const expect = chai.expect; const Liquid = require('..'); const mock = require('mock-fs'); -chai.use(require("chai-as-promised")); + +chai.use(chaiAsPromised); + var liquid = Liquid({ root: '/', extname: '.html' @@ -13,17 +16,14 @@ var liquid = Liquid({ ctx, src, dst; function test(src, dst) { - liquid.parseAndRender(src, ctx) + return liquid.parseAndRender(src, ctx) .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) { - expect(function() { - liquid.parseAndRender(src, ctx); - }).to.throw(pattern); + return liquid.parseAndRender(src, ctx).should.eventually.be.rejectedWith(pattern); } describe('tags', function() { @@ -67,76 +67,63 @@ describe('tags', function() { mock.restore(); }); - it('should support assign', function() { - test('{% assign foo="bar" %}{{foo}}', 'bar'); - test('{% assign foo=(1..3) %}{{foo}}', '[1,2,3]'); - test('{% assign foo="a b" | capitalize | split: " " | first %}{{foo}}', 'A'); - }); + it('should support assign 1', function() { return test('{% assign foo="bar" %}{{foo}}', 'bar'); }); + it('should support assign 2', function() { return 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'); }); - it('should support raw', function() { - testThrow('{% raw%}', /{% raw%} not closed/); - test('{% raw %}{{ 5 | plus: 6 }}{% endraw %} is equal to 11.', '{{ 5 | plus: 6 }} is equal to 11.'); - test('{% raw %}\n{{ foo}} \n{% endraw %}', '\n{{ foo}} \n'); - }); + it('should support raw 1', function() { return 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.'); }); + it('should support raw 3', function() { return 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() { - testThrow('{% case "foo"%}', /{% case "foo"%} not closed/); - test('{% case "foo"%}' + - '{% 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 comment 1', function() { return testThrow('{% comment %}{% raw%}', /{% comment %} not closed/); }); + it('should support comment 2', function() { return test('My name is {% comment %}super{% endcomment %} Shopify.', 'My name is Shopify.'); }); + it('should support comment 3', function() { return test('{% comment %}\n{{ foo}} \n{% endcomment %}', ''); }); - it('should support if', function() { - testThrow('{% if false%}yes', /tag {% if false%} not closed/); - test('{%if emptyArray%}a{%endif%}', ''); - test('{% if 2==3 %}yes{%else%}no{%endif%}', 'no'); - test('{% if 1>=2 and one| 4 | 5 |