refactor: use promise.mapSeries

This commit is contained in:
harttle
2016-11-01 22:24:56 +08:00
parent f8269f4d20
commit 951225fb75
13 changed files with 103 additions and 124 deletions
+3 -2
View File
@@ -10,6 +10,7 @@ See:
* [supported filter list](https://github.com/harttle/shopify-liquid/wiki/Builtin-Filters), and
* [supported tag list](https://github.com/harttle/shopify-liquid/wiki/Builtin-Tags)
* any-promise
Installation:
@@ -108,7 +109,7 @@ Note: includes and layouts lookup path should always be specified by `Liquid({ro
## Use in Browser
[Download][releases] the dist files and import into your HTML.
And `window.Liquid` is what you want.
And `window.Liquid` is what you want. There's also a [demo](demo/browser/).
```html
<html lang="en">
@@ -131,7 +132,7 @@ And `window.Liquid` is what you want.
</html>
```
There's also a [demo](demo/browser/).
Note: any-promise browser requires a polyfill or explicit registration. e.g: `require('any-promise/register/bluebird')`
## Includes
+15 -8
View File
@@ -14,6 +14,7 @@ const tags = require('./tags');
const filters = require('./filters');
const Promise = require('any-promise');
const anySeries = require('./src/util/promise.js').anySeries;
const Errors = require('./src/util/error.js');
var _engine = {
init: function(tag, filter, options) {
@@ -47,14 +48,15 @@ var _engine = {
return this.renderer.renderTemplates(tpl, scope, opts);
},
parseAndRender: function(html, ctx, opts) {
try {
var tpl = this.parse(html);
return this.render(tpl, ctx, opts);
} 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);
}
return Promise.resolve()
.then(() => this.parse(html))
.then(tpl => this.render(tpl, ctx, opts))
.catch(e => {
if (e instanceof Errors.RenderBreak) {
return e.html;
}
throw e;
});
},
renderFile: function(filepath, ctx, opts) {
return this.getTemplate(filepath)
@@ -137,5 +139,10 @@ factory.isTruthy = Syntax.isTruthy;
factory.isFalsy = Syntax.isFalsy;
factory.evalExp = Syntax.evalExp;
factory.evalValue = Syntax.evalValue;
factory.Types = {
ParseError: Errors.ParseError,
TokenizationEroor: Errors.TokenizationError,
RenderBreak: Errors.RenderBreak
};
module.exports = factory;
+1 -1
View File
@@ -1,5 +1,5 @@
const lexical = require('./lexical.js');
const ParseError = require('./error.js').ParseError;
const ParseError = require('./util/error.js').ParseError;
module.exports = function(Tag, Filter) {
+25 -41
View File
@@ -1,5 +1,7 @@
const Syntax = require('./syntax.js');
const Promise = require('any-promise');
const mapSeries = require('./util/promise.js').mapSeries;
const RenderBreak = require('./util/error.js').RenderBreak;
var render = {
@@ -9,54 +11,36 @@ var render = {
opts.strict_filters = opts.strict_filters || false;
var html = '';
return mapSeries(templates, (tpl) => {
return renderTemplate.call(this, tpl)
.then(partial => html += partial)
.catch(e => {
if(e instanceof RenderBreak){
e.resolvedHTML = html;
}
throw e;
});
}).then(() => 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...
// emptyPromise.then(renderTag(template0).then(renderTag(template1).then(renderTag(template2)...
var lastPromise = templates.reduce((promise, template) => {
return promise.then(() => {
var promiseLink = Promise.resolve('');
switch (template.type) {
case 'tag':
// Add Promises to the chain
promiseLink = this.renderTag(template, scope, this.register)
.then((partial) => {
if (partial === undefined) {
return true; // basically a noop (do nothing)
}
return html += partial;
});
break;
case 'html':
promiseLink = Promise.resolve(template.value)
.then((partial) => {
return html += partial;
});
break;
case 'output':
var val = this.evalOutput(template, scope, opts);
promiseLink = Promise.resolve(val === undefined ? '' : stringify(val))
.then((partial) => {
return html += partial;
});
break;
}
return promiseLink;
});
}, 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, that's the promise returned from this.renderTag or a resolved promise with raw html.
return lastPromise;
function renderTemplate(template){
if (template.type === 'tag') {
return this.renderTag(template, scope, this.register)
.then(partial => partial === undefined ? '' : partial);
} else if (template.type === 'output') {
return Promise.resolve(this.evalOutput(template, scope, opts))
.then(partial => partial === undefined ? '' : stringify(partial));
} else { // template.type === 'html'
return Promise.resolve(template.value);
}
}
},
renderTag: function(template, scope, register) {
if (template.name === 'continue') {
return Promise.resolve('');
return Promise.reject(new RenderBreak('continue'));
}
if (template.name === 'break') {
return Promise.reject(new Error('forloop.stop')); // this will stop the sequential promise chain
return Promise.reject(new RenderBreak('break'));
}
return template.render(scope, register);
},
+1 -1
View File
@@ -1,5 +1,5 @@
const lexical = require('./lexical.js');
const TokenizationError = require('./error.js').TokenizationError;
const TokenizationError = require('./util/error.js').TokenizationError;
const _ = require('./util/underscore.js');
function parse(html) {
+17 -3
View File
@@ -1,5 +1,7 @@
function TokenizationError(message, input, line) {
Error.captureStackTrace(this, this.constructor);
if(Error.captureStackTrace){
Error.captureStackTrace(this, this.constructor);
}
this.name = this.constructor.name;
this.message = message;
@@ -10,7 +12,9 @@ TokenizationError.prototype = Object.create(Error.prototype);
TokenizationError.prototype.constructor = TokenizationError;
function ParseError(message, input, line, e) {
Error.captureStackTrace(this, this.constructor);
if(Error.captureStackTrace){
Error.captureStackTrace(this, this.constructor);
}
this.name = this.constructor.name;
this.originalError = e;
@@ -21,6 +25,16 @@ function ParseError(message, input, line, e) {
ParseError.prototype = Object.create(Error.prototype);
ParseError.prototype.constructor = ParseError;
function RenderBreak(message){
if(Error.captureStackTrace){
Error.captureStackTrace(this, this.constructor);
}
this.name = this.constructor.name;
this.message = message;
}
RenderBreak.prototype = Object.create(Error.prototype);
RenderBreak.prototype.constructor = RenderBreak;
module.exports = {
TokenizationError, ParseError
TokenizationError, ParseError, RenderBreak
};
+29 -54
View File
@@ -1,7 +1,9 @@
var Liquid = require('..');
var Promise = require('any-promise');
var lexical = Liquid.lexical;
var re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+` +
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 re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+` +
`(${lexical.value.source})` +
`(?:\\s+${lexical.hash.source})*` +
`(?:\\s+(reversed))?$`);
@@ -37,19 +39,14 @@ module.exports = function(liquid) {
return liquid.renderer.renderTemplates(this.elseTemplates, scope);
}
var html = '';
var length = collection.length;
var offset = hash.offset || 0;
var limit = (hash.limit === undefined) ? collection.length : hash.limit;
collection = collection.slice(offset, offset + limit);
if(this.reversed) collection.reverse();
if (this.reversed) collection.reverse();
// for needs to execute the promises sequentially, not just resolve them sequentially, due to break and continue.
// We can't just loop through executing everything then resolve them all sequentially like we do for render.renderTemplates
// First, we build the array of parameters we are going to use for each call to renderTemplates
var contexts = [];
collection.some((item, i) => {
var contexts = collection.map((item, i) => {
var ctx = {};
ctx[this.variable] = item;
ctx.forloop = {
@@ -63,51 +60,29 @@ module.exports = function(liquid) {
stop: false,
skip: false
};
// We are just putting together an array of the arguments we will be passing to our sequential promises
contexts.push(ctx);
return ctx;
});
// This is some pretty tricksy javascript, at least to me.
// 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...
// emptyPromise.then(renderTemplates(args0).then(renderTemplates(args1).then(renderTemplates(args2)...
var lastPromise = contexts.reduce((promise, context) => {
return promise.then((partial) => {
if (scope.get('forloop.stop')) {
throw new Error('forloop.stop'); // this will stop the sequential promise chain
}
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.
scope.push(context);
return liquid.renderer.renderTemplates(this.templates, scope);
})
.then((partial) => {
scope.pop(context);
return partial;
});
}, 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((partial) => {
return html += partial;
})
.catch((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;
}
});
var html = '';
return mapSeries(contexts, (context) => {
scope.push(context);
return liquid.renderer
.renderTemplates(this.templates, scope)
.then(partial => html += partial)
.catch(e => {
if (e instanceof RenderBreak) {
html += e.resolvedHTML;
if (e.message === 'continue') return;
}
throw e;
})
.then(() => scope.pop());
}).catch((e) => {
if (e instanceof RenderBreak && e.message === 'break') {
return;
}
throw e;
}).then(() => html);
}
});
};
+4 -3
View File
@@ -6,18 +6,19 @@ chai.use(require("chai-as-promised"));
describe('tags/capture', function() {
var liquid = Liquid();
it('should support capture 1', function() {
it('should support capture', function() {
var src = '{% capture f %}{{"a" | capitalize}}{%endcapture%}{{f}}';
return expect(liquid.parseAndRender(src))
.to.eventually.equal('A');
});
it('should support capture 2', function() {
it('should throw on invalid identifier', function() {
var src = '{% capture = %}{%endcapture%}';
return expect(liquid.parseAndRender(src))
.to.be.rejectedWith(/= not valid identifier/);
});
it('should throw when for capture closed', function() {
it('should throw when capture not closed', function() {
var src = '{%capture c%}{{c}}';
return expect(liquid.parseAndRender(src))
.to.be.rejectedWith(/tag .* not closed/);
+2 -4
View File
@@ -46,12 +46,10 @@ describe('tags/for', function() {
it('should support for with continue', function() {
var src = '{% for i in (1..5) %}' +
'{% if i == 4 %}{% continue %}' +
'{% else %}{{ i }}' +
'{% endif %}' +
'{{i}}{% continue %}after' +
'{% endfor %}';
return expect(liquid.parseAndRender(src, ctx))
.to.eventually.equal('1235');
.to.eventually.equal('12345');
});
it('should support for with break', function() {
src = '{% for i in (one..5) %}' +
+2 -2
View File
@@ -2,7 +2,7 @@ const Liquid = require('../..');
const mock = require('mock-fs');
const chai = require("chai");
const expect = chai.expect;
const error = require('../../src/error.js');
const ParseError = Liquid.Types.ParseError;
chai.use(require("chai-as-promised"));
describe('tags/include', function() {
@@ -52,7 +52,7 @@ describe('tags/include', function() {
it('should throw when illegal', function() {
return expect(liquid.renderFile('/illegal.html')).to.
be.rejectedWith(error.ParseError, /illegal token {%include%}/);
be.rejectedWith(ParseError, /illegal token {%include%}/);
});
it('should support include with relative path', function() {
-1
View File
@@ -2,7 +2,6 @@ const Liquid = require('../..');
const mock = require('mock-fs');
const chai = require("chai");
const expect = chai.expect;
const error = require('../../src/error.js');
chai.use(require("chai-as-promised"));
describe('tags/layout', function() {
-1
View File
@@ -1,7 +1,6 @@
const Liquid = require('../..');
const chai = require("chai");
const expect = chai.expect;
const error = require('../../src/error.js');
chai.use(require("chai-as-promised"));
describe('tags/tablerow', function() {
+4 -3
View File
@@ -1,8 +1,9 @@
var chai = require("chai");
var expect = chai.expect;
var engine = require('..')(), ctx;
const chai = require("chai");
const expect = chai.expect;
const mock = require('mock-fs');
var engine = require('../..')(), ctx;
function test(promise, cb){
return promise
.then((result) => {