mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-20 15:00:42 -07:00
refactor: use promise.mapSeries
This commit is contained in:
@@ -10,6 +10,7 @@ See:
|
|||||||
|
|
||||||
* [supported filter list](https://github.com/harttle/shopify-liquid/wiki/Builtin-Filters), and
|
* [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)
|
* [supported tag list](https://github.com/harttle/shopify-liquid/wiki/Builtin-Tags)
|
||||||
|
* any-promise
|
||||||
|
|
||||||
Installation:
|
Installation:
|
||||||
|
|
||||||
@@ -108,7 +109,7 @@ Note: includes and layouts lookup path should always be specified by `Liquid({ro
|
|||||||
## Use in Browser
|
## Use in Browser
|
||||||
|
|
||||||
[Download][releases] the dist files and import into your HTML.
|
[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
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@@ -131,7 +132,7 @@ And `window.Liquid` is what you want.
|
|||||||
</html>
|
</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
|
## Includes
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const tags = require('./tags');
|
|||||||
const filters = require('./filters');
|
const filters = require('./filters');
|
||||||
const Promise = require('any-promise');
|
const Promise = require('any-promise');
|
||||||
const anySeries = require('./src/util/promise.js').anySeries;
|
const anySeries = require('./src/util/promise.js').anySeries;
|
||||||
|
const Errors = require('./src/util/error.js');
|
||||||
|
|
||||||
var _engine = {
|
var _engine = {
|
||||||
init: function(tag, filter, options) {
|
init: function(tag, filter, options) {
|
||||||
@@ -47,14 +48,15 @@ var _engine = {
|
|||||||
return this.renderer.renderTemplates(tpl, scope, opts);
|
return this.renderer.renderTemplates(tpl, scope, opts);
|
||||||
},
|
},
|
||||||
parseAndRender: function(html, ctx, opts) {
|
parseAndRender: function(html, ctx, opts) {
|
||||||
try {
|
return Promise.resolve()
|
||||||
var tpl = this.parse(html);
|
.then(() => this.parse(html))
|
||||||
return this.render(tpl, ctx, opts);
|
.then(tpl => this.render(tpl, ctx, opts))
|
||||||
} catch (error) {
|
.catch(e => {
|
||||||
// A throw inside of a then or catch of a Promise automatically rejects, but since we mix a sync call
|
if (e instanceof Errors.RenderBreak) {
|
||||||
// with an async call, we need to do this in case the sync call throws.
|
return e.html;
|
||||||
return Promise.reject(error);
|
}
|
||||||
}
|
throw e;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
renderFile: function(filepath, ctx, opts) {
|
renderFile: function(filepath, ctx, opts) {
|
||||||
return this.getTemplate(filepath)
|
return this.getTemplate(filepath)
|
||||||
@@ -137,5 +139,10 @@ factory.isTruthy = Syntax.isTruthy;
|
|||||||
factory.isFalsy = Syntax.isFalsy;
|
factory.isFalsy = Syntax.isFalsy;
|
||||||
factory.evalExp = Syntax.evalExp;
|
factory.evalExp = Syntax.evalExp;
|
||||||
factory.evalValue = Syntax.evalValue;
|
factory.evalValue = Syntax.evalValue;
|
||||||
|
factory.Types = {
|
||||||
|
ParseError: Errors.ParseError,
|
||||||
|
TokenizationEroor: Errors.TokenizationError,
|
||||||
|
RenderBreak: Errors.RenderBreak
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = factory;
|
module.exports = factory;
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
const lexical = require('./lexical.js');
|
const lexical = require('./lexical.js');
|
||||||
const ParseError = require('./error.js').ParseError;
|
const ParseError = require('./util/error.js').ParseError;
|
||||||
|
|
||||||
module.exports = function(Tag, Filter) {
|
module.exports = function(Tag, Filter) {
|
||||||
|
|
||||||
|
|||||||
+25
-41
@@ -1,5 +1,7 @@
|
|||||||
const Syntax = require('./syntax.js');
|
const Syntax = require('./syntax.js');
|
||||||
const Promise = require('any-promise');
|
const Promise = require('any-promise');
|
||||||
|
const mapSeries = require('./util/promise.js').mapSeries;
|
||||||
|
const RenderBreak = require('./util/error.js').RenderBreak;
|
||||||
|
|
||||||
var render = {
|
var render = {
|
||||||
|
|
||||||
@@ -9,54 +11,36 @@ var render = {
|
|||||||
opts.strict_filters = opts.strict_filters || false;
|
opts.strict_filters = opts.strict_filters || false;
|
||||||
|
|
||||||
var html = '';
|
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
|
function renderTemplate(template){
|
||||||
// It's fundamentally equivalent to the following...
|
if (template.type === 'tag') {
|
||||||
// emptyPromise.then(renderTag(template0).then(renderTag(template1).then(renderTag(template2)...
|
return this.renderTag(template, scope, this.register)
|
||||||
var lastPromise = templates.reduce((promise, template) => {
|
.then(partial => partial === undefined ? '' : partial);
|
||||||
return promise.then(() => {
|
} else if (template.type === 'output') {
|
||||||
var promiseLink = Promise.resolve('');
|
return Promise.resolve(this.evalOutput(template, scope, opts))
|
||||||
switch (template.type) {
|
.then(partial => partial === undefined ? '' : stringify(partial));
|
||||||
case 'tag':
|
} else { // template.type === 'html'
|
||||||
// Add Promises to the chain
|
return Promise.resolve(template.value);
|
||||||
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;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
renderTag: function(template, scope, register) {
|
renderTag: function(template, scope, register) {
|
||||||
if (template.name === 'continue') {
|
if (template.name === 'continue') {
|
||||||
return Promise.resolve('');
|
return Promise.reject(new RenderBreak('continue'));
|
||||||
}
|
}
|
||||||
if (template.name === 'break') {
|
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);
|
return template.render(scope, register);
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
const lexical = require('./lexical.js');
|
const lexical = require('./lexical.js');
|
||||||
const TokenizationError = require('./error.js').TokenizationError;
|
const TokenizationError = require('./util/error.js').TokenizationError;
|
||||||
const _ = require('./util/underscore.js');
|
const _ = require('./util/underscore.js');
|
||||||
|
|
||||||
function parse(html) {
|
function parse(html) {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
function TokenizationError(message, input, line) {
|
function TokenizationError(message, input, line) {
|
||||||
Error.captureStackTrace(this, this.constructor);
|
if(Error.captureStackTrace){
|
||||||
|
Error.captureStackTrace(this, this.constructor);
|
||||||
|
}
|
||||||
this.name = this.constructor.name;
|
this.name = this.constructor.name;
|
||||||
|
|
||||||
this.message = message;
|
this.message = message;
|
||||||
@@ -10,7 +12,9 @@ TokenizationError.prototype = Object.create(Error.prototype);
|
|||||||
TokenizationError.prototype.constructor = TokenizationError;
|
TokenizationError.prototype.constructor = TokenizationError;
|
||||||
|
|
||||||
function ParseError(message, input, line, e) {
|
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.name = this.constructor.name;
|
||||||
this.originalError = e;
|
this.originalError = e;
|
||||||
|
|
||||||
@@ -21,6 +25,16 @@ function ParseError(message, input, line, e) {
|
|||||||
ParseError.prototype = Object.create(Error.prototype);
|
ParseError.prototype = Object.create(Error.prototype);
|
||||||
ParseError.prototype.constructor = ParseError;
|
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 = {
|
module.exports = {
|
||||||
TokenizationError, ParseError
|
TokenizationError, ParseError, RenderBreak
|
||||||
};
|
};
|
||||||
+29
-54
@@ -1,7 +1,9 @@
|
|||||||
var Liquid = require('..');
|
const Liquid = require('..');
|
||||||
var Promise = require('any-promise');
|
const Promise = require('any-promise');
|
||||||
var lexical = Liquid.lexical;
|
const lexical = Liquid.lexical;
|
||||||
var re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+` +
|
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})` +
|
`(${lexical.value.source})` +
|
||||||
`(?:\\s+${lexical.hash.source})*` +
|
`(?:\\s+${lexical.hash.source})*` +
|
||||||
`(?:\\s+(reversed))?$`);
|
`(?:\\s+(reversed))?$`);
|
||||||
@@ -37,19 +39,14 @@ module.exports = function(liquid) {
|
|||||||
return liquid.renderer.renderTemplates(this.elseTemplates, scope);
|
return liquid.renderer.renderTemplates(this.elseTemplates, scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
var html = '';
|
|
||||||
var length = collection.length;
|
var 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;
|
||||||
|
|
||||||
collection = collection.slice(offset, offset + 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.
|
var contexts = collection.map((item, i) => {
|
||||||
// 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 ctx = {};
|
var ctx = {};
|
||||||
ctx[this.variable] = item;
|
ctx[this.variable] = item;
|
||||||
ctx.forloop = {
|
ctx.forloop = {
|
||||||
@@ -63,51 +60,29 @@ module.exports = function(liquid) {
|
|||||||
stop: false,
|
stop: false,
|
||||||
skip: false
|
skip: false
|
||||||
};
|
};
|
||||||
// We are just putting together an array of the arguments we will be passing to our sequential promises
|
return ctx;
|
||||||
contexts.push(ctx);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// This is some pretty tricksy javascript, at least to me.
|
var html = '';
|
||||||
// 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
|
return mapSeries(contexts, (context) => {
|
||||||
// It's fundamentally equivalent to the following...
|
scope.push(context);
|
||||||
// emptyPromise.then(renderTemplates(args0).then(renderTemplates(args1).then(renderTemplates(args2)...
|
return liquid.renderer
|
||||||
var lastPromise = contexts.reduce((promise, context) => {
|
.renderTemplates(this.templates, scope)
|
||||||
return promise.then((partial) => {
|
.then(partial => html += partial)
|
||||||
if (scope.get('forloop.stop')) {
|
.catch(e => {
|
||||||
throw new Error('forloop.stop'); // this will stop the sequential promise chain
|
if (e instanceof RenderBreak) {
|
||||||
}
|
html += e.resolvedHTML;
|
||||||
|
if (e.message === 'continue') return;
|
||||||
return html += partial;
|
}
|
||||||
})
|
throw e;
|
||||||
.then((partial) => {
|
})
|
||||||
// todo: Make sure our scope management is sound here. Create some tests that revolve around loops
|
.then(() => scope.pop());
|
||||||
// with sections that take differing amounts of time to complete. Make sure the order is maintained
|
}).catch((e) => {
|
||||||
// and scope doesn't bleed over into other renderTemplate calls.
|
if (e instanceof RenderBreak && e.message === 'break') {
|
||||||
scope.push(context);
|
return;
|
||||||
return liquid.renderer.renderTemplates(this.templates, scope);
|
}
|
||||||
})
|
throw e;
|
||||||
.then((partial) => {
|
}).then(() => html);
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,18 +6,19 @@ chai.use(require("chai-as-promised"));
|
|||||||
describe('tags/capture', function() {
|
describe('tags/capture', function() {
|
||||||
var liquid = Liquid();
|
var liquid = Liquid();
|
||||||
|
|
||||||
it('should support capture 1', function() {
|
it('should support capture', function() {
|
||||||
var src = '{% capture f %}{{"a" | capitalize}}{%endcapture%}{{f}}';
|
var src = '{% capture f %}{{"a" | capitalize}}{%endcapture%}{{f}}';
|
||||||
return expect(liquid.parseAndRender(src))
|
return expect(liquid.parseAndRender(src))
|
||||||
.to.eventually.equal('A');
|
.to.eventually.equal('A');
|
||||||
});
|
});
|
||||||
it('should support capture 2', function() {
|
|
||||||
|
it('should throw on invalid identifier', function() {
|
||||||
var src = '{% capture = %}{%endcapture%}';
|
var src = '{% capture = %}{%endcapture%}';
|
||||||
return expect(liquid.parseAndRender(src))
|
return expect(liquid.parseAndRender(src))
|
||||||
.to.be.rejectedWith(/= not valid identifier/);
|
.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}}';
|
var src = '{%capture c%}{{c}}';
|
||||||
return expect(liquid.parseAndRender(src))
|
return expect(liquid.parseAndRender(src))
|
||||||
.to.be.rejectedWith(/tag .* not closed/);
|
.to.be.rejectedWith(/tag .* not closed/);
|
||||||
|
|||||||
+2
-4
@@ -46,12 +46,10 @@ describe('tags/for', function() {
|
|||||||
|
|
||||||
it('should support for with continue', function() {
|
it('should support for with continue', function() {
|
||||||
var src = '{% for i in (1..5) %}' +
|
var src = '{% for i in (1..5) %}' +
|
||||||
'{% if i == 4 %}{% continue %}' +
|
'{{i}}{% continue %}after' +
|
||||||
'{% else %}{{ i }}' +
|
|
||||||
'{% endif %}' +
|
|
||||||
'{% endfor %}';
|
'{% endfor %}';
|
||||||
return expect(liquid.parseAndRender(src, ctx))
|
return expect(liquid.parseAndRender(src, ctx))
|
||||||
.to.eventually.equal('1235');
|
.to.eventually.equal('12345');
|
||||||
});
|
});
|
||||||
it('should support for with break', function() {
|
it('should support for with break', function() {
|
||||||
src = '{% for i in (one..5) %}' +
|
src = '{% for i in (one..5) %}' +
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const Liquid = require('../..');
|
|||||||
const mock = require('mock-fs');
|
const mock = require('mock-fs');
|
||||||
const chai = require("chai");
|
const chai = require("chai");
|
||||||
const expect = chai.expect;
|
const expect = chai.expect;
|
||||||
const error = require('../../src/error.js');
|
const ParseError = Liquid.Types.ParseError;
|
||||||
chai.use(require("chai-as-promised"));
|
chai.use(require("chai-as-promised"));
|
||||||
|
|
||||||
describe('tags/include', function() {
|
describe('tags/include', function() {
|
||||||
@@ -52,7 +52,7 @@ describe('tags/include', function() {
|
|||||||
|
|
||||||
it('should throw when illegal', function() {
|
it('should throw when illegal', function() {
|
||||||
return expect(liquid.renderFile('/illegal.html')).to.
|
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() {
|
it('should support include with relative path', function() {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ const Liquid = require('../..');
|
|||||||
const mock = require('mock-fs');
|
const mock = require('mock-fs');
|
||||||
const chai = require("chai");
|
const chai = require("chai");
|
||||||
const expect = chai.expect;
|
const expect = chai.expect;
|
||||||
const error = require('../../src/error.js');
|
|
||||||
chai.use(require("chai-as-promised"));
|
chai.use(require("chai-as-promised"));
|
||||||
|
|
||||||
describe('tags/layout', function() {
|
describe('tags/layout', function() {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
const Liquid = require('../..');
|
const Liquid = require('../..');
|
||||||
const chai = require("chai");
|
const chai = require("chai");
|
||||||
const expect = chai.expect;
|
const expect = chai.expect;
|
||||||
const error = require('../../src/error.js');
|
|
||||||
chai.use(require("chai-as-promised"));
|
chai.use(require("chai-as-promised"));
|
||||||
|
|
||||||
describe('tags/tablerow', function() {
|
describe('tags/tablerow', function() {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
var chai = require("chai");
|
const chai = require("chai");
|
||||||
var expect = chai.expect;
|
const expect = chai.expect;
|
||||||
var engine = require('..')(), ctx;
|
|
||||||
const mock = require('mock-fs');
|
const mock = require('mock-fs');
|
||||||
|
|
||||||
|
var engine = require('../..')(), ctx;
|
||||||
|
|
||||||
function test(promise, cb){
|
function test(promise, cb){
|
||||||
return promise
|
return promise
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
Reference in New Issue
Block a user