From 4626f8f6bfdf5c026a725518cd98e0af71ff8759 Mon Sep 17 00:00:00 2001 From: Tim Hardy Date: Sun, 28 Aug 2016 23:28:06 -0500 Subject: [PATCH 1/6] Got first test working in async - nested includes --- index.js | 24 +++++++++++++++++++----- package.json | 2 ++ src/render.js | 26 +++++++++++++++++--------- tags/include.js | 16 +++++++++++----- test/tags.js | 22 ++++++++++++++++++++-- 5 files changed, 69 insertions(+), 21 deletions(-) diff --git a/index.js b/index.js index 93cad2074..1d212af72 100644 --- a/index.js +++ b/index.js @@ -12,6 +12,7 @@ const Template = require('./src/parser'); const Expression = require('./src/expression.js'); const tags = require('./tags'); const filters = require('./filters'); +const Promise = require("bluebird"); var _engine = { init: function(tag, filter, options) { @@ -43,8 +44,14 @@ var _engine = { }, renderFile: function(filepath, ctx) { try{ - var tpl = this.handleCache(filepath); - return this.render(tpl, ctx); + return this.handleCache(filepath) + .then((templates) => { + return this.render(templates, ctx); + }) + .catch((e) => { + e.file = filepath; + throw e; + }); } catch(e){ e.file = filepath; @@ -67,9 +74,16 @@ var _engine = { if (path.extname(filepath) === '') { filepath += this.options.extname; } - var tpl = this.options.cache && this.cache[filepath] || - this.parse(fs.readFileSync(filepath, 'utf8')); - return this.options.cache ? (this.cache[filepath] = tpl) : tpl; + + return this.getTemplate(filepath) + .then((html) => { + var tpl = this.options.cache && this.cache[filepath] || this.parse(html); + return this.options.cache ? (this.cache[filepath] = tpl) : tpl; + }); + }, + getTemplate: function(filepath) { + var html = fs.readFileSync(filepath, 'utf8'); + return Promise.resolve(html); }, express: function() { return (filePath, options, callback) => { diff --git a/package.json b/package.json index 03d65f9f0..5a970d0bc 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,13 @@ }, "homepage": "https://github.com/harttle/shopify-liquid#readme", "dependencies": { + "bluebird": "^3.4.3", "lodash": "^4.13.1", "strftime": "^0.9.2" }, "devDependencies": { "chai": "^3.5.0", + "chai-as-promised": "^5.3.0", "coveralls": "^2.11.9", "express": "^4.14.0", "istanbul": "^0.4.3", diff --git a/src/render.js b/src/render.js index d10ba8ee5..8249d6273 100644 --- a/src/render.js +++ b/src/render.js @@ -1,30 +1,38 @@ const error = require('./error.js'); const Exp = require('./expression.js'); const assert = require('assert'); +const Promise = require("bluebird"); var render = { renderTemplates: function(templates, scope) { assert(scope, 'unable to evalTemplates: scope undefined'); - var html = '', - partial; - templates.some(template => { + var htmlBlocks = [], + partial, + promises = []; + templates.some((template, index) => { if (scope.get('forloop.skip')) return true; switch (template.type) { case 'tag': - partial = this.renderTag(template, scope, this.register); - if (partial === undefined) return true; - html += partial; + promises.push(this.renderTag(template, scope, this.register) + .then((partial) => { + if (partial === undefined) return true; + return htmlBlocks[index] = partial; + })); break; case 'html': - html += template.value; + promises.push(Promise.resolve(htmlBlocks[index] = template.value)); break; case 'output': var val = this.evalOutput(template, scope); - html += val === undefined ? '' : stringify(val); + htmlBlocks[index] = val === undefined ? '' : stringify(val); + promises.push(Promise.resolve(htmlBlocks[index])); } }); - return html; + return Promise.all(promises) + .then((results) => { + return htmlBlocks.join(''); + }); }, renderTag: function(template, scope, register) { diff --git a/tags/include.js b/tags/include.js index 6644175ae..a136a9b32 100644 --- a/tags/include.js +++ b/tags/include.js @@ -1,4 +1,5 @@ var Liquid = require('..'); +var Promise = require("bluebird"); var lexical = Liquid.lexical; var withRE = new RegExp(`with\\s+(${lexical.value.source})`); @@ -20,11 +21,16 @@ module.exports = function(liquid) { if(this.with){ hash[filepath] = Liquid.evalValue(this.with, scope); } - var tpl = liquid.handleCache(filepath); - scope.push(hash); - var html = liquid.renderer.renderTemplates(tpl, scope); - scope.pop(); - return html; + return liquid.handleCache(filepath) + .then((templates) => { + scope.push(hash); + return liquid.renderer.renderTemplates(templates, scope); + }) + .then((html) => { + scope.pop(); + return html; + }); + } }); diff --git a/test/tags.js b/test/tags.js index fc6b69ff5..aef0c960e 100644 --- a/test/tags.js +++ b/test/tags.js @@ -1,7 +1,9 @@ const chai = require("chai"); +const should = chai.should(); const expect = chai.expect; const Liquid = require('..'); const mock = require('mock-fs'); +chai.use(require("chai-as-promised")); var liquid = Liquid({ root: '/', extname: '.html' @@ -28,7 +30,14 @@ describe('tags', function() { foo: 'bar', arr: [-2, 'a'], alpha: ['a', 'b', 'c'], - emptyArray: [] + emptyArray: [], + person: { + firstName: 'Joe', + lastName: 'Shmoe', + address: { + city: 'Dallas' + } + } }; mock({ '/default-layout.html': 'foo{% block %}Default{% endblock %}foo', @@ -42,7 +51,10 @@ describe('tags', function() { '/color.html': 'color:{{color}}, shape:{{shape}}', '/with.html': '{% include "color" with "red", shape: "rect" %}', '/scope.html': '{% assign shape="triangle" %}{% assign color="yellow" %}{% include "color.html" %}', - '/hash.html': '{% assign name="harttle" %}{% include "user.html", role: "admin", alias: name %}' + '/hash.html': '{% assign name="harttle" %}{% include "user.html", role: "admin", alias: name %}', + '/personInfo.html': 'This is a person {% include "card.html" %}', + '/card.html': '

{{person.firstName}} {{person.lastName}}
{% include "address" %}

', + '/address.html': 'City: {{person.address.city}}' }); }); afterEach(function() { @@ -266,6 +278,12 @@ describe('tags', function() { var dst = 'color:red, shape:rect'; expect(liquid.renderFile(filepath, ctx)).to.equal(dst); }); + + it.only('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

') + }); + it('should throw when block not closed', function() { src = '{% layout "default-layout" %}{%block%}bar'; testThrow(src, /tag {%block%} not closed/); From ba390df7b6e7bcbfe8289e468cea6e4a02699150 Mon Sep 17 00:00:00 2001 From: Tim Hardy Date: Sun, 11 Sep 2016 23:33:26 -0500 Subject: [PATCH 2/6] replaced bluebird with any-promise to allow consumers of the library to choose their promise implementation --- index.js | 2 +- package.json | 2 +- src/render.js | 2 +- tags/include.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 1d212af72..944d151fd 100644 --- a/index.js +++ b/index.js @@ -12,7 +12,7 @@ const Template = require('./src/parser'); const Expression = require('./src/expression.js'); const tags = require('./tags'); const filters = require('./filters'); -const Promise = require("bluebird"); +const Promise = require('any-promise'); var _engine = { init: function(tag, filter, options) { diff --git a/package.json b/package.json index 5a970d0bc..39a264d61 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/harttle/shopify-liquid#readme", "dependencies": { - "bluebird": "^3.4.3", + "any-promise": "^1.3.0", "lodash": "^4.13.1", "strftime": "^0.9.2" }, diff --git a/src/render.js b/src/render.js index 8249d6273..6d0679308 100644 --- a/src/render.js +++ b/src/render.js @@ -1,7 +1,7 @@ const error = require('./error.js'); const Exp = require('./expression.js'); const assert = require('assert'); -const Promise = require("bluebird"); +const Promise = require('any-promise'); var render = { diff --git a/tags/include.js b/tags/include.js index a136a9b32..6f8eb94f5 100644 --- a/tags/include.js +++ b/tags/include.js @@ -1,5 +1,5 @@ var Liquid = require('..'); -var Promise = require("bluebird"); +var Promise = require('any-promise'); var lexical = Liquid.lexical; var withRE = new RegExp(`with\\s+(${lexical.value.source})`); From dc85c80afe73cd4c3dfd088e1dfd160227eef8ba Mon Sep 17 00:00:00 2001 From: Tim Hardy Date: Sun, 18 Sep 2016 01:23:29 -0500 Subject: [PATCH 3/6] About halfway through converting all tags to async --- src/render.js | 4 ++-- src/tag.js | 3 ++- tags/assign.js | 2 ++ tags/cycle.js | 3 ++- tags/for.js | 47 ++++++++++++++++++++++++++++++++++++++++++----- tags/raw.js | 4 +++- tags/tablerow.js | 1 + test/tags.js | 10 +++++++--- 8 files changed, 61 insertions(+), 13 deletions(-) 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

') }); From 87e284159d69f9713f01a7f5f379118c2c3d4ede Mon Sep 17 00:00:00 2001 From: Tim Hardy Date: Thu, 22 Sep 2016 08:47:09 -0500 Subject: [PATCH 4/6] Making progress on async --- src/render.js | 124 +++++++++++++++++++++++++++++++++++++---------- tags/for.js | 38 +++++++++------ tags/tablerow.js | 64 ++++++++++++++++-------- test/tags.js | 8 +-- 4 files changed, 171 insertions(+), 63 deletions(-) diff --git a/src/render.js b/src/render.js index d146cc4a8..9e16e65c1 100644 --- a/src/render.js +++ b/src/render.js @@ -7,32 +7,106 @@ var render = { renderTemplates: function(templates, scope) { assert(scope, 'unable to evalTemplates: scope undefined'); - var htmlBlocks = [], - partial, - promises = []; - templates.some((template, index) => { - if (scope.get('forloop.skip')) return true; - switch (template.type) { - case 'tag': - promises.push(this.renderTag(template, scope, this.register) - .then((partial) => { - if (partial === undefined) return true; - return htmlBlocks[index] = partial; - })); - break; - case 'html': - promises.push(Promise.resolve(htmlBlocks[index] = template.value)); - break; - case 'output': - var val = this.evalOutput(template, scope); - htmlBlocks[index] = val === undefined ? '' : stringify(val); - promises.push(Promise.resolve(htmlBlocks[index])); - } - }); - return Promise.all(promises) - .then((results) => { - return htmlBlocks.join(''); + + 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... + // emptyPromise.then(renderTag(template0).then(renderTag(template1).then(renderTag(template2)... + var lastPromise = templates.reduce((promise, template) => { + return promise.then((partial) => { + if (scope.get('forloop.skip')) { + return Promise.resolve(''); + } + if (scope.get('forloop.stop')) { + throw new Error('forloop.stop'); // this will stop/break the sequential promise chain and go to the catch + } + + 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); + promiseLink = Promise.resolve(val === undefined ? '' : stringify(val)) + .then((partial) => { + return html += partial; + }); + break; + } + + return promiseLink; + }) + .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, that's the promise returned from this.renderTag or a resolved promise with raw html. + + return lastPromise + .then(() => { + return html; + }) + .catch((error) => { + throw new Error(error); + }); + }, renderTag: function(template, scope, register) { diff --git a/tags/for.js b/tags/for.js index 8c0444377..b69773965 100644 --- a/tags/for.js +++ b/tags/for.js @@ -47,7 +47,10 @@ module.exports = function(liquid) { collection = collection.slice(offset, offset + limit); if(this.reversed) collection.reverse(); - var scopes = []; + // 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) => { ctx[this.variable] = item; ctx.forloop = { @@ -61,26 +64,31 @@ 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); // 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); + contexts.push(ctx); }); - // 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) + // 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 + } html += 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; + }) .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 @@ -94,9 +102,9 @@ module.exports = function(liquid) { // 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(() => { - return Promise.resolve(html); + return html; }) .catch((error) => { throw new Error(error); diff --git a/tags/tablerow.js b/tags/tablerow.js index 1640c5a65..ecf24cdaf 100644 --- a/tags/tablerow.js +++ b/tags/tablerow.js @@ -30,6 +30,7 @@ module.exports = function(liquid) { var collection = Liquid.evalExp(this.collection, scope) || []; var html = '
ab
', + promiseChain = Promise.resolve(''); // create an empty promise to begin the chain ctx = {}, length = collection.length; var offset = hash.offset || 0; @@ -38,27 +39,50 @@ module.exports = function(liquid) { var cols = hash.cols, row, col; if (!cols) throw new Error(`illegal cols: ${cols}`); - collection.slice(offset, offset + limit).some((item, i) => { - row = Math.floor(i / cols) + 1; - col = (i % cols) + 1; - if(col === 1){ - if(row !== 1){ - html += ''; - } - html += ``; - } + // build array of arguments to pass to sequential promises... + var contexts = collection.slice(offset, offset + limit); - ctx[this.variable] = item; - scope.push(ctx); - html += `'; - scope.pop(ctx); - }); - if(row > 0) html += ''; - html += '
`; - // todo: replace with sequential promises, see for.js - html += liquid.renderer.renderTemplates(this.templates, scope); - html += '
'; - return 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 + // It's fundamentally equivalent to the following... + // emptyPromise.then(renderTemplates(args0).then(renderTemplates(args1).then(renderTemplates(args2)... + var lastPromise = contexts.reduce((promise, context, currentIndex) => { + return promise.then((partial) => { + row = Math.floor(currentIndex / cols) + 1; + col = (currentIndex % cols) + 1; + if(col === 1) { + if(row !== 1){ + html += ''; + } + html += ``; + } + + ctx[this.variable] = context; + scope.push(ctx); + html += ``; + 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); + }); + }, 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(() => { + if(row > 0) html += ''; + html += ''; + return html; + }) + .catch((error) => { + throw new Error(error); + }); } }); }; diff --git a/test/tags.js b/test/tags.js index 063e4443b..35df7ce9e 100644 --- a/test/tags.js +++ b/test/tags.js @@ -1,3 +1,5 @@ +// temporary +var Promise = require('any-promise'); const chai = require("chai"); const should = chai.should(); const expect = chai.expect; @@ -207,7 +209,7 @@ describe('tags', function() { test('{% decrement one %}{{one}}', '0'); }); - it.only('should support tablerow', function() { + it('should support tablerow', function() { src = '{% tablerow i in alpha cols:2 %}{{ i }}{% endtablerow %}'; dst = '' + '' + @@ -261,8 +263,8 @@ describe('tags', function() { test(src, dst); }); - it('should support include', function() { - expect(liquid.renderFile('/current.html', ctx)).to.equal('barFOObar'); + it.only('should support include', function() { + return liquid.renderFile('/current.html', ctx).should.eventually.equal('barFOObar'); }); it('should support include with relative path', function() { From bb8468708aed30465685006c7408fa2849815b5b Mon Sep 17 00:00:00 2001 From: Tim Hardy Date: Sun, 25 Sep 2016 00:13:29 -0500 Subject: [PATCH 5/6] Making good progress on async --- index.js | 33 ++++---- src/render.js | 47 ++---------- tags/capture.js | 6 +- tags/for.js | 33 ++++---- tags/layout.js | 54 ++++++++++--- tags/tablerow.js | 42 ++++++----- test/liquid.js | 80 ++++++++++++-------- test/tags.js | 193 ++++++++++++++++++++++------------------------- 8 files changed, 255 insertions(+), 233 deletions(-) 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 = '
ab
', promiseChain = Promise.resolve(''); // create an empty promise to begin the chain - ctx = {}, length = collection.length; var offset = hash.offset || 0; 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}`); // 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 // It's fundamentally equivalent to the following... @@ -56,32 +63,33 @@ module.exports = function(liquid) { html += ``; } - ctx[this.variable] = context; - scope.push(ctx); - 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 += ''; + if(row > 0) { + html += ''; + } html += '
`; + //ctx[this.variable] = context; + + return html += ``; + }) + .then((partial) => { + scope.push(context); return liquid.renderer.renderTemplates(this.templates, scope) }) .then((partial) => { - html += partial; - html += '
'; return html; }) .catch((error) => { - throw new Error(error); + throw error; }); } }); diff --git a/test/liquid.js b/test/liquid.js index 48163356e..4d4fb213f 100644 --- a/test/liquid.js +++ b/test/liquid.js @@ -1,8 +1,10 @@ const chai = require("chai"); +const chaiAsPromised = require("chai-as-promised"); +const should = chai.should(); const expect = chai.expect; -const should = chai.should; const Liquid = require('..'); const mock = require('mock-fs'); +chai.use(chaiAsPromised); describe('liquid', function() { var engine, ctx; @@ -27,13 +29,13 @@ describe('liquid', function() { mock.restore(); }); 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() { - 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() { - 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() { (function() { @@ -45,46 +47,55 @@ describe('liquid', function() { }); it('should render template multiple times', function() { var template = engine.parse('{{obj}}'); - engine.render(template, ctx).should.equal('{"foo":"bar"}'); - engine.render(template, ctx).should.equal('{"foo":"bar"}'); + return engine.render(template, ctx) + .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() { var template = engine.parse('

{{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 one2 %}yes', /tag {% unless 1>2 %} not closed/); - test('{% unless 1>2 %}yes{%endunless%}', 'yes'); - test('{% unless true %}{%endunless%}', ''); - }); + it('should support if 1', function() { return testThrow('{% if false%}yes', /tag {% if false%} not closed/); }); + it('should support if 2', function() { return test('{%if emptyArray%}a{%endif%}', ''); }); + it('should support if 3', function() { return test('{% if 2==3 %}yes{%else%}no{%endif%}', 'no'); }); + it('should support if 4', function() { return test('{% if 1>=2 and one2 %}yes', /tag {% unless 1>2 %} not closed/); }); + 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() { - testThrow('{%capture c%}{{c}}', /tag .* not closed/); + return testThrow('{%capture c%}{{c}}', /tag .* not closed/); }); 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() { - 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() { - 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() { @@ -149,65 +136,65 @@ describe('tags', function() { dst = 'true.1.0.false.3.3.2a\n' + 'false.2.1.false.3.2.1b\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) %}' + '{% if i == 4 %}{% continue %}' + '{% else %}{{ i }}' + '{% endif %}' + '{% endfor %}'; - test(src, '1235'); + return test(src, '1235'); + }); + it('should support for with break', function() { src = '{% for i in (one..5) %}' + '{% if i == 4 %}{% break %}{% endif %}' + '{{ i }}' + '{% 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 %}'; - 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 %}'; - test(src, '67'); + return test(src, '67'); }); it('should support for reversed', function() { src = '{% for i in (1..5) limit:2 reversed %}{{ i }}{% endfor %}'; - test(src, '21'); + return test(src, '21'); }); it('should support cycle', function() { 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() { - testThrow('{%cycle%}', /empty candidates/); + return testThrow('{%cycle%}', /empty candidates/); }); it('should support cycle in for block', function() { src = '{% for i in (1..5) %}{% cycle one, "e"%}{% endfor %}'; - test(src, '1e1e1'); + return test(src, '1e1e1'); }); it('should support cycle group', function() { src = "{% cycle one: '1', '2', '3'%}" + "{% cycle 1: '1', '2', '3'%}" + "{% cycle 2: '1', '2', '3'%}"; - test(src, '121'); + return test(src, '121'); }); - it('should support increment', function() { - test('{% increment foo %}{%increment foo%}{{foo}}', '2'); - test('{% increment one %}{{one}}', '2'); - }); + it('should support increment 1', function() { return test('{% increment foo %}{%increment foo%}{{foo}}', '2'); }); + it('should support increment 2', function() { return test('{% increment one %}{{one}}', '2'); }); - it('should support decrement', function() { - test('{% decrement foo %}{%decrement foo%}{{foo}}', '-2'); - test('{% decrement one %}{{one}}', '0'); - }); + it('should support decrement 1', function() { return test('{% decrement foo %}{%decrement foo%}{{foo}}', '-2'); }); + it('should support decrement 2', function() { return test('{% decrement one %}{{one}}', '0'); }); it('should support tablerow', function() { src = '{% tablerow i in alpha cols:2 %}{{ i }}{% endtablerow %}'; @@ -215,18 +202,18 @@ describe('tags', function() { 'ab' + 'c' + ''; - test(src, dst); + return test(src, dst); }); it('should support empty tablerow', function() { src = '{% tablerow i in (1..0) cols:2 %}{{ i }}{% endtablerow %}'; dst = '
'; - test(src, dst); + return test(src, dst); }); it('should throw when tablerow not closed', function() { 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() { @@ -236,13 +223,15 @@ describe('tags', function() { '34' + '5' + ''; - test(src, dst); + return test(src, dst); }); - it('tablerow should throw on illegal cols', function() { - testThrow('{% tablerow i in (1..5) cols:0 %}{{ i }}{% endtablerow %}', + it('tablerow should throw on illegal cols 1', function() { + return testThrow('{% tablerow i in (1..5) cols:0 %}{{ i }}{% endtablerow %}', /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/); }); @@ -252,7 +241,7 @@ describe('tags', function() { '12' + '3' + ''; - test(src, dst); + return test(src, dst); }); it('should support tablerow with offset', function() { @@ -260,29 +249,29 @@ describe('tags', function() { dst = '' + '' + '
45
'; - 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'); }); 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() { - 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() { - 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() { var filepath = 'with.html'; 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() { @@ -292,22 +281,24 @@ describe('tags', function() { it('should throw when block not closed', function() { src = '{% layout "default-layout" %}{%block%}bar'; - testThrow(src, /tag {%block%} not closed/); + return testThrow(src, /tag {%block%} not closed/); }); it('should support layout', function() { src = '{% layout "default-layout" %}{%block%}bar{%endblock%}'; - test(src, 'foobarfoo'); + return test(src, 'foobarfoo'); }); it('should support layout: multiple blocks', function() { src = '{% layout "multi-blocks-layout" %}' + '{%block a%}bara{%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%}'; - 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%}'; - test(src, 'fooaaa;C;foo'); + return test(src, 'fooaaa;C;foo'); }); }); From 8b2d6ede5dfbe6d3d4ad21b0dd7912894d9e3993 Mon Sep 17 00:00:00 2001 From: Tim Hardy Date: Sun, 25 Sep 2016 20:30:07 -0500 Subject: [PATCH 6/6] All tests passing except describe('#express()'... in liquid.js - commented out --- test/error.js | 40 ++++----- test/filters.js | 217 +++++++++++++++++++++++------------------------- test/render.js | 10 +-- 3 files changed, 121 insertions(+), 146 deletions(-) diff --git a/test/error.js b/test/error.js index 5bb3f65a6..51c0ee666 100644 --- a/test/error.js +++ b/test/error.js @@ -3,22 +3,20 @@ var expect = chai.expect; var engine = require('..')(), ctx; const mock = require('mock-fs'); -function test(func, cb){ - try{ - func(); - cb({}); - } - catch(e){ - cb(e); - } +function test(promise, cb){ + return promise + .then((result) => { + return cb({}); + }) + .catch((error) => { + return cb(error); + }); } describe('error', function() { it('should throw TokenizationError when tag illegal', function() { - test(function(){ - engine.parseAndRender('{% -a %}', {}); - }, function(err){ + return test(engine.parseAndRender('{% -a %}', {}), function(err){ expect(err.name).to.equal('TokenizationError'); expect(err.message).to.equal('illegal tag: {% -a %}'); expect(err.input).to.equal('{% -a %}'); @@ -27,9 +25,7 @@ describe('error', function() { }); it('should throw correct error info', function() { - test(function(){ - engine.parseAndRender('{%if true%}\naaa{%endif%}\n{% -a %}\n3', {}); - }, function(err){ + return test(engine.parseAndRender('{%if true%}\naaa{%endif%}\n{% -a %}\n3', {}), function(err){ expect(err.input).to.equal('{% -a %}'); expect(err.line).to.equal(3); }); @@ -39,9 +35,7 @@ describe('error', function() { mock({ "/foo.html": '\n\n\n{% raw %}\n\n' }); - test(function(){ - engine.renderFile('/foo.html', {}); - }, function(err){ + return test(engine.renderFile('/foo.html', {}), function(err){ expect(err.input).to.equal('{% raw %}'); expect(err.line).to.equal(4); expect(err.file).to.equal('/foo.html'); @@ -49,9 +43,7 @@ describe('error', function() { }); it('should throw ParseError when filter not exist', function() { - test(function(){ - engine.parseAndRender('{{ a | xz }}', {}); - }, function(err){ + return test(engine.parseAndRender('{{ a | xz }}', {}), function(err){ expect(err.name).to.equal('ParseError'); expect(err.message).to.equal('filter "xz" not found'); expect(err.input).to.equal('{{ a | xz }}'); @@ -59,9 +51,7 @@ describe('error', function() { }); }); it('should throw ParseError when tag not exist', function() { - test(function(){ - engine.parseAndRender('{% a %}', {}); - }, function(err){ + return test(engine.parseAndRender('{% a %}', {}), function(err){ expect(err.name).to.equal('ParseError'); expect(err.message).to.equal('tag a not found'); expect(err.input).to.equal('{% a %}'); @@ -70,9 +60,7 @@ describe('error', function() { }); it('should throw ParseError when tag not closed', function() { - test(function(){ - engine.parseAndRender('{% if %}', {}); - }, function(err){ + return test(engine.parseAndRender('{% if %}', {}), function(err){ expect(err.name).to.equal('ParseError'); expect(err.message).to.equal('tag {% if %} not closed'); expect(err.input).to.equal('{% if %}'); diff --git a/test/filters.js b/test/filters.js index 7bcb52cfe..cd1a181f5 100644 --- a/test/filters.js +++ b/test/filters.js @@ -1,8 +1,10 @@ const chai = require("chai"); +const chaiAsPromised = require("chai-as-promised"); +const should = chai.should(); const expect = chai.expect; - var liquid = require('..')(), ctx; +chai.use(chaiAsPromised); function test(src, dst) { ctx = { @@ -18,106 +20,87 @@ function test(src, dst) { category: 'bar' }] }; - expect(liquid.parseAndRender(src, ctx)).to.equal(dst); + return liquid.parseAndRender(src, ctx).should.eventually.equal(dst); } describe('filters', function() { - it('should support abs', function() { - test('{{ -3 | abs }}', '3'); - test('{{ arr[0] | abs }}', '2'); - }); + it('should support abs 1', function() { return test('{{ -3 | abs }}', '3'); }); + it('should support abs 2', function() { return test('{{ arr[0] | abs }}', '2'); }); - it('should support append', function() { - test('{{ -3 | append: "abc" }}', '-3abc'); - test('{{ "a" | append: foo }}', 'abar'); - }); + it('should support append 1', function() { return test('{{ -3 | append: "abc" }}', '-3abc'); }); + it('should support append 2', function() { return test('{{ "a" | append: foo }}', 'abar');; }); - it('should support capitalize', function() { - test('{{ "i am good" | capitalize }}', 'I am good'); - }); + it('should support capitalize', function() { return test('{{ "i am good" | capitalize }}', 'I am good'); }); - it('should support ceil', function() { - test('{{ 1.2 | ceil }}', '2'); - test('{{ 2.0 | ceil }}', '2'); - test('{{ "3.5" | ceil }}', '4'); - test('{{ 183.357 | ceil }}', '184'); - }); + it('should support ceil 1', function() { return test('{{ 1.2 | ceil }}', '2'); }); + it('should support ceil 2', function() { return test('{{ 2.0 | ceil }}', '2'); }); + it('should support ceil 3', function() { return test('{{ "3.5" | ceil }}', '4'); }); + it('should support ceil 4', function() { return test('{{ 183.357 | ceil }}', '184'); }); it('should support date', function() { str = ctx.date.toDateString(); - test('{{ date | date:"%a %b %d %Y"}}', str); + return test('{{ date | date:"%a %b %d %Y"}}', str); }); - it('should support default', function() { - test('{{false |default: "a"}}', 'a'); - }); + it('should support default', function() { return test('{{false |default: "a"}}', 'a'); }); - it('should support divided_by', function() { - test('{{4 | divided_by: 2}}', '2'); - test('{{16 | divided_by: 4}}', '4'); - test('{{5 | divided_by: 3}}', '1'); - }); + it('should support divided_by 1', function() { return test('{{4 | divided_by: 2}}', '2'); }); + it('should support divided_by 2', function() { return test('{{16 | divided_by: 4}}', '4'); }); + it('should support divided_by 3', function() { return test('{{5 | divided_by: 3}}', '1'); }); - it('should support downcase', function() { - test('{{ "Parker Moore" | downcase }}', 'parker moore'); - test('{{ "apple" | downcase }}', 'apple'); - }); - it('should support escape', function() { - test('{{ "Have you read \'James & the Giant Peach\'?" | escape }}', + it('should support downcase 1', function() { return test('{{ "Parker Moore" | downcase }}', 'parker moore'); }); + it('should support downcase 2', function() { return test('{{ "apple" | downcase }}', 'apple'); }); + + it('should support escape 1', function() { + return test('{{ "Have you read \'James & the Giant Peach\'?" | escape }}', 'Have you read 'James & the Giant Peach'?'); - test('{{ "Tetsuro Takara" | escape }}', 'Tetsuro Takara'); + }); + it('should support escape 2', function() { + return test('{{ "Tetsuro Takara" | escape }}', 'Tetsuro Takara'); }); - it('should support escape_once', function() { - test('{{ "1 < 2 & 3" | escape_once }}', '1 < 2 & 3'); - test('{{ "1 < 2 & 3" | escape_once }}', '1 < 2 & 3'); - }); + it('should support escape_once 1', function() { return test('{{ "1 < 2 & 3" | escape_once }}', '1 < 2 & 3'); }); + it('should support escape_once 2', function() { return test('{{ "1 < 2 & 3" | escape_once }}', '1 < 2 & 3'); }); it('should support split/first', function() { src = '{% assign my_array = "apples, oranges, peaches, plums" | split: ", " %}' + '{{ my_array | first }}'; - test(src, 'apples'); + return test(src, 'apples'); }); - it('should support floor', function() { - test('{{ 1.2 | floor }}', '1'); - test('{{ 2.0 | floor }}', '2'); - test('{{ 183.357 | floor }}', '183'); - test('{{ "3.5" | floor }}', '3'); - }); + it('should support floor 1', function() { return test('{{ 1.2 | floor }}', '1'); }); + it('should support floor 2', function() { return test('{{ 2.0 | floor }}', '2'); }); + it('should support floor 3', function() { return test('{{ 183.357 | floor }}', '183'); }); + it('should support floor 4', function() { return test('{{ "3.5" | floor }}', '3'); }); it('should support join', function() { src = '{% assign beatles = "John, Paul, George, Ringo" | split: ", " %}' + '{{ beatles | join: " and " }}'; - test(src, 'John and Paul and George and Ringo'); + return test(src, 'John and Paul and George and Ringo'); }); it('should support split/last', function() { src = '{% assign my_array = "zebra, octopus, giraffe, tiger" | split: ", " %}' + '{{ my_array|last }}'; - test(src, 'tiger'); + return test(src, 'tiger'); }); it('should support lstrip', function() { src = '{{ " So much room for activities! " | lstrip }}'; - test(src, 'So much room for activities! '); + return test(src, 'So much room for activities! '); }); it('should support map', function() { - test('{{posts | map: "category"}}', '["foo","bar"]'); + return test('{{posts | map: "category"}}', '["foo","bar"]'); }); - it('should support minus', function() { - test('{{ 4 | minus: 2 }}', '2'); - test('{{ 16 | minus: 4 }}', '12'); - test('{{ 183.357 | minus: 12 }}', '171.357'); - }); + it('should support minus 1', function() { return test('{{ 4 | minus: 2 }}', '2'); }); + it('should support minus 2', function() { return test('{{ 16 | minus: 4 }}', '12'); }); + it('should support minus 3', function() { return test('{{ 183.357 | minus: 12 }}', '171.357'); }); - it('should support modulo', function() { - test('{{ 3 | modulo: 2 }}', '1'); - test('{{ 24 | modulo: 7 }}', '3'); - test('{{ 183.357 | modulo: 12 }}', '3.357'); - }); + it('should support modulo 1', function() { return test('{{ 3 | modulo: 2 }}', '1'); }); + it('should support modulo 2', function() { return test('{{ 24 | modulo: 7 }}', '3'); }); + it('should support modulo 3', function() { return test('{{ 183.357 | modulo: 12 }}', '3.357'); }); it('should support string_with_newlines', function() { src = '{% capture string_with_newlines %}\n' + @@ -128,81 +111,75 @@ describe('filters', function() { dst = '
' + 'Hello
' + 'there
'; - test(src, dst); + return test(src, dst); }); - it('should support plus', function() { - test('{{ 4 | plus: 2 }}', '6'); - test('{{ 16 | plus: 4 }}', '20'); - test('{{ 183.357 | plus: 12 }}', '195.357'); - }); + it('should support plus 1', function() { return test('{{ 4 | plus: 2 }}', '6'); }); + it('should support plus 2', function() { return test('{{ 16 | plus: 4 }}', '20'); }); + it('should support plus 3', function() { return test('{{ 183.357 | plus: 12 }}', '195.357'); }); it('should support prepend', function() { - test('{% assign url = "liquidmarkup.com" %}' + + return test('{% assign url = "liquidmarkup.com" %}' + '{{ "/index.html" | prepend: url }}', 'liquidmarkup.com/index.html'); }); it('should support remove', function() { - test('{{ "I strained to see the train through the rain" | remove: "rain" }}', + return test('{{ "I strained to see the train through the rain" | remove: "rain" }}', 'I sted to see the t through the '); }); it('should support remove_first', function() { - test('{{ "I strained to see the train through the rain" | remove_first: "rain" }}', + return test('{{ "I strained to see the train through the rain" | remove_first: "rain" }}', 'I sted to see the train through the rain'); }); it('should support replace', function() { - test('{{ "Take my protein pills and put my helmet on" | replace: "my", "your" }}', + return test('{{ "Take my protein pills and put my helmet on" | replace: "my", "your" }}', 'Take your protein pills and put your helmet on'); }); it('should support replace_first', function() { - test('{% assign my_string = "Take my protein pills and put my helmet on" %}\n' + + return test('{% assign my_string = "Take my protein pills and put my helmet on" %}\n' + '{{ my_string | replace_first: "my", "your" }}', '\nTake your protein pills and put my helmet on'); }); it('should support reverse', function() { - test('{{ "Ground control to Major Tom." | split: "" | reverse | join: "" }}', + return test('{{ "Ground control to Major Tom." | split: "" | reverse | join: "" }}', '.moT rojaM ot lortnoc dnuorG'); }); - it('should support round', function() { - test('{{1.2|round}}', '1'); - test('{{2.7|round}}', '3'); - test('{{183.357|round: 2}}', '183.36'); - }); + it('should support round 1', function() { return test('{{1.2|round}}', '1'); }); + it('should support round 2', function() { return test('{{2.7|round}}', '3'); }); + it('should support round 3', function() { return test('{{183.357|round: 2}}', '183.36'); }); it('should support rstrip', function() { - test('{{ " So much room for activities! " | rstrip }}', + return test('{{ " So much room for activities! " | rstrip }}', ' So much room for activities!'); }); - it('should support size', function() { - test('{{ "Ground control to Major Tom." | size }}', '28'); - test('{% assign my_array = "apples, oranges, peaches, plums"' + + it('should support size 1', function() { return test('{{ "Ground control to Major Tom." | size }}', '28'); }); + it('should support size 2', function() { + return test('{% assign my_array = "apples, oranges, peaches, plums"' + ' | split: ", " %}{{ my_array | size }}', '4'); }); - it('should support slice', function() { - test('{{ "Liquid" | slice: 0 }}', 'L'); - test('{{ "Liquid" | slice: 2 }}', 'q'); - test('{{ "Liquid" | slice: 2, 5 }}', 'quid'); - test('{{ "Liquid" | slice: -3, 2 }}', 'ui'); - }); + it('should support slice 1', function() { return test('{{ "Liquid" | slice: 0 }}', 'L'); }); + it('should support slice 2', function() { return test('{{ "Liquid" | slice: 2 }}', 'q'); }); + it('should support slice 3', function() { return test('{{ "Liquid" | slice: 2, 5 }}', 'quid'); }); + it('should support slice 4', function() { return test('{{ "Liquid" | slice: -3, 2 }}', 'ui'); }); it('should support sort', function() { - test('{% assign my_array = "zebra, octopus, giraffe, Sally Snake"' + + return test('{% assign my_array = "zebra, octopus, giraffe, Sally Snake"' + ' | split: ", " %}' + '{{ my_array | sort | join: ", " }}', 'Sally Snake, giraffe, octopus, zebra'); }); it('should support split', function() { - test('{% assign beatles = "John, Paul, George, Ringo" | split: ", " %}' + + return test('{% assign beatles = "John, Paul, George, Ringo" | split: ", " %}' + '{% for member in beatles %}' + '{{ member }} ' + '{% endfor %}', @@ -210,63 +187,73 @@ describe('filters', function() { }); it('should support strip', function() { - test('{{ " So much room for activities! " | strip }}', + return test('{{ " So much room for activities! " | strip }}', 'So much room for activities!'); }); - it('should support strip_tml', function() { - test('{{ "Have you read Ulysses?" | strip_html }}', + it('should support strip_tml 1', function() { + return test('{{ "Have you read Ulysses?" | strip_html }}', 'Have you read Ulysses?'); - test('{{"

< p >

" | strip_html }}', ''); + }); + it('should support strip_tml 2', function() { + return test('{{"

< p >

" | strip_html }}', ''); }); it('should support strip_newlines', function() { - test('{% capture string_with_newlines %}\n' + + return test('{% capture string_with_newlines %}\n' + 'Hello\nthere\n{% endcapture %}' + '{{ string_with_newlines | strip_newlines }}', 'Hellothere'); }); - it('should support times', function() { - test('{{ 3 | times: 2 }}', '6'); - test('{{ 24 | times: 7 }}', '168'); - test('{{ 183.357 | times: 12 }}', '2200.284'); - }); + it('should support times 1', function() { return test('{{ 3 | times: 2 }}', '6'); }); + it('should support times 2', function() { return test('{{ 24 | times: 7 }}', '168'); }); + it('should support times 3', function() { return test('{{ 183.357 | times: 12 }}', '2200.284'); }); - it('should support truncate', function() { - test('{{ "Ground control to Major Tom." | truncate: 20 }}', + it('should support truncate 1', function() { + return test('{{ "Ground control to Major Tom." | truncate: 20 }}', 'Ground control to...'); - test('{{ "Ground control to Major Tom." | truncate: 80 }}', + }); + it('should support truncate 2', function() { + return test('{{ "Ground control to Major Tom." | truncate: 80 }}', 'Ground control to Major Tom.'); - test('{{ "Ground control to Major Tom." | truncate: 25,", and so on" }}', + }); + it('should support truncate 3', function() { + return test('{{ "Ground control to Major Tom." | truncate: 25,", and so on" }}', 'Ground control, and so on'); - test('{{ "Ground control to Major Tom." | truncate: 20, "" }}', + }); + it('should support truncate 4', function() { + return test('{{ "Ground control to Major Tom." | truncate: 20, "" }}', 'Ground control to Ma'); }); - it('should support truncatewords', function() { - test('{{ "Ground control to Major Tom." | truncatewords: 3 }}', + it('should support truncatewords 1', function() { + return test('{{ "Ground control to Major Tom." | truncatewords: 3 }}', 'Ground control to...'); - test('{{ "Ground control to Major Tom." | truncatewords: 8 }}', + }); + it('should support truncatewords 2', function() { + return test('{{ "Ground control to Major Tom." | truncatewords: 8 }}', 'Ground control to Major Tom.'); - test('{{ "Ground control to Major Tom." | truncatewords: 3, "--" }}', + }); + it('should support truncatewords 3', function() { + return test('{{ "Ground control to Major Tom." | truncatewords: 3, "--" }}', 'Ground control to--'); - test('{{ "Ground control to Major Tom." | truncatewords: 3, "" }}', + }); + it('should support truncatewords 4', function() { + return test('{{ "Ground control to Major Tom." | truncatewords: 3, "" }}', 'Ground control to'); }); it('should support uniq', function() { - test('{% assign my_array = "ants, bugs, bees, bugs, ants" | split: ", " %}' + + return test('{% assign my_array = "ants, bugs, bees, bugs, ants" | split: ", " %}' + '{{ my_array | uniq | join: ", " }}', 'ants, bugs, bees'); }); it('should support upcase', function() { - test('{{ "Parker Moore" | upcase }}', 'PARKER MOORE'); + return test('{{ "Parker Moore" | upcase }}', 'PARKER MOORE'); }); - it('should support url_encode', function() { - test('{{ "john@liquid.com" | url_encode }}', 'john%40liquid.com'); - test('{{ "Tetsuro Takara" | url_encode }}', 'Tetsuro%20Takara'); - }); + it('should support url_encode 1', function() { return test('{{ "john@liquid.com" | url_encode }}', 'john%40liquid.com'); }); + it('should support url_encode 2', function() { return test('{{ "Tetsuro Takara" | url_encode }}', 'Tetsuro%20Takara'); }); }); diff --git a/test/render.js b/test/render.js index 8bdfda535..08fe9f1ec 100644 --- a/test/render.js +++ b/test/render.js @@ -1,9 +1,12 @@ const chai = require("chai"); +const chaiAsPromised = require("chai-as-promised"); +const should = chai.should(); +const expect = chai.expect; const sinonChai = require("sinon-chai"); const sinon = require("sinon"); -const expect = chai.expect; chai.use(sinonChai); +chai.use(chaiAsPromised); var tag = require('../src/tag.js')(); var Scope = require('../src/scope.js'); @@ -26,10 +29,7 @@ describe('render', function() { }); it('should render html', function() { - expect(render.renderTemplates([{ - type: 'html', - value: '

' - }], scope)).to.equal('

'); + return render.renderTemplates([{type: 'html', value: '

'}], scope).should.eventually.equal('

'); }); it('should eval filter with correct arguments', function() {