diff --git a/src/render.js b/src/render.js index 6d0679308..d146cc4a8 100644 --- a/src/render.js +++ b/src/render.js @@ -38,12 +38,12 @@ var render = { renderTag: function(template, scope, register) { if (template.name === 'continue') { scope.set('forloop.skip', true); - return; + return Promise.resolve(''); } if (template.name === 'break') { scope.set('forloop.stop', true); scope.set('forloop.skip', true); - return; + return Promise.resolve(''); } return template.render(scope, register); }, diff --git a/src/tag.js b/src/tag.js index d3cf22029..e2cdb0c03 100644 --- a/src/tag.js +++ b/src/tag.js @@ -1,4 +1,5 @@ const lexical = require('./lexical.js'); +const Promise = require('any-promise'); const Exp = require('./expression.js'); const TokenizationError = require('./error.js').TokenizationError; @@ -21,7 +22,7 @@ module.exports = function() { var reg = register[this.name]; if(!reg) reg = register[this.name] = {}; var obj = hash(this.token.args, scope); - return this.tagImpl.render && this.tagImpl.render(scope, obj, reg) || ''; + return this.tagImpl.render && this.tagImpl.render(scope, obj, reg) || Promise.resolve(''); }, parse: function(token, tokens){ this.type = 'tag'; diff --git a/tags/assign.js b/tags/assign.js index effc31ac4..d63f72343 100644 --- a/tags/assign.js +++ b/tags/assign.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*=(.*)`); @@ -13,6 +14,7 @@ module.exports = function(liquid) { }, render: function(scope, hash) { scope.set(this.key, liquid.evalOutput(this.value, scope)); + return Promise.resolve(''); } }); diff --git a/tags/cycle.js b/tags/cycle.js index 3ad73a606..d1ef96cf7 100644 --- a/tags/cycle.js +++ b/tags/cycle.js @@ -1,4 +1,5 @@ var Liquid = require('..'); +var Promise = require('any-promise'); var lexical = Liquid.lexical; var groupRE = new RegExp(`^(?:(${lexical.value.source})\\s*:\\s*)?(.*)$`); var candidatesRE = new RegExp(lexical.value.source, 'g'); @@ -37,7 +38,7 @@ module.exports = function(liquid) { idx = (idx + 1) % this.candidates.length; register[fingerprint] = idx; - return Liquid.evalValue(candidate, scope); + return Promise.resolve(Liquid.evalValue(candidate, scope)); } }); }; diff --git a/tags/for.js b/tags/for.js index ddbecc7c6..8c0444377 100644 --- a/tags/for.js +++ b/tags/for.js @@ -1,4 +1,6 @@ var Liquid = require('..'); +var Promise = require('any-promise'); +var _ = require('lodash'); var lexical = Liquid.lexical; var re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+` + `(${lexical.value.source})` + @@ -44,6 +46,8 @@ module.exports = function(liquid) { collection = collection.slice(offset, offset + limit); if(this.reversed) collection.reverse(); + + var scopes = []; collection.some((item, i) => { ctx[this.variable] = item; ctx.forloop = { @@ -57,14 +61,47 @@ module.exports = function(liquid) { stop: false, skip: false }; + // todo: verify scope management is good here. Make sure we don't consume too many resources here with the clone. + // Is there a simpler solution? scope.push(ctx); - html += liquid.renderer.renderTemplates(this.templates, scope); - var breakloop = scope.get('forloop.stop'); + // We are just putting together an array of the arguments we will be passing to our sequential promises + scopes.push(_.clone(scope)); scope.pop(ctx); - - if (breakloop) return true; }); - return html; + + // This is some pretty tricksy javascript, at least to me. Bluebird would have made this a lot easier, but + // we are trying to not use anything that can't be done in native node Promises. + // This basically just processes an array of promises sequentially for every argument in the 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 + var lastPromise = scopes.reduce((promise, scope) => { + return promise.then(function(partial) { + var breakloop = scope.get('forloop.stop'); + if (breakloop) + throw new Error('forloop.stop'); // this will stop the sequential promise chain + + html += partial; + return liquid.renderer.renderTemplates(this.templates, scope); + }) + .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 + // in our reduce callback will be the returned promise from our "then" above. In this + // case, the promise returned from liquid.renderer.renderTemplates. + + lastPromise + .then(() => { + return Promise.resolve(html); + }) + .catch((error) => { + throw new Error(error); + }); + } }); }; diff --git a/tags/raw.js b/tags/raw.js index ac5120601..1f6fe097e 100644 --- a/tags/raw.js +++ b/tags/raw.js @@ -1,4 +1,5 @@ var Liquid = require('..'); +var Promise = require('any-promise'); var lexical = Liquid.lexical; var re = new RegExp(`(${lexical.identifier.source})`); @@ -20,7 +21,8 @@ module.exports = function(liquid) { stream.start(); }, render: function(scope, hash) { - return this.tokens.map(token => token.raw).join(''); + var tokens = this.tokens.map(token => token.raw).join(''); + return Promise.resolve(tokens); } }); diff --git a/tags/tablerow.js b/tags/tablerow.js index 8cc24cd91..1640c5a65 100644 --- a/tags/tablerow.js +++ b/tags/tablerow.js @@ -51,6 +51,7 @@ module.exports = function(liquid) { ctx[this.variable] = item; scope.push(ctx); html += ``; + // todo: replace with sequential promises, see for.js html += liquid.renderer.renderTemplates(this.templates, scope); html += ''; scope.pop(ctx); diff --git a/test/tags.js b/test/tags.js index aef0c960e..063e4443b 100644 --- a/test/tags.js +++ b/test/tags.js @@ -11,7 +11,11 @@ var liquid = Liquid({ ctx, src, dst; function test(src, dst) { - expect(liquid.parseAndRender(src, ctx)).to.equal(dst); + liquid.parseAndRender(src, ctx) + .then((result) => { + expect(result.to.equal(dst)); + }); + //expect(liquid.parseAndRender(src, ctx)).to.equal(dst); } function testThrow(src, pattern) { @@ -203,7 +207,7 @@ describe('tags', function() { test('{% decrement one %}{{one}}', '0'); }); - it('should support tablerow', function() { + it.only('should support tablerow', function() { src = '{% tablerow i in alpha cols:2 %}{{ i }}{% endtablerow %}'; dst = '' + '' + @@ -279,7 +283,7 @@ describe('tags', function() { expect(liquid.renderFile(filepath, ctx)).to.equal(dst); }); - it.only('should support nested includes', function() { + it('should support nested includes', function() { //expect(liquid.renderFile('personInfo.html', ctx)).to.equal('This is a person

Joe Shmoe
City: Dallas

'); return liquid.renderFile('personInfo.html', ctx).should.eventually.equal('This is a person

Joe Shmoe
City: Dallas

') });
ab