diff --git a/README.md b/README.md index db4f561e8..d60b100e3 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,9 @@ var html = engine.renderFile("hello.html", {name: 'alice'}); `cache` default to `false`, `extname` default to `.liquid`, `root` default to `""`. -## Include +## Includes and Layouts + +### Includes ``` // file: color.liquid @@ -70,6 +72,30 @@ color: 'red' shape: 'circle' color: 'yellow' shape: 'square' ``` +### Layouts + +``` +// file: default-layout.html +Header +{% block content %}My default content{% endblock %} +Footer + +// file: page.html +{% layout "default-layout" %} +{% block content %}My page content{% endblock %} +``` + +The output of `page.html`: + +``` +Header +My page content +Footer +``` + +* It's possible to define multiple blocks. +* block name is optional when there's only one block. + ## Extension Register Filters: @@ -131,6 +157,7 @@ Tag | Document | Source | Test `raw` | [Document](https://help.shopify.com/themes/liquid/tags/theme-tags#raw) | [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/raw.js) | [Test][tt] `comment` | [Document](https://help.shopify.com/themes/liquid/tags/theme-tags#comment) | [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/comment.js) | [Test][tt] `include` | [Document](https://help.shopify.com/themes/liquid/tags/theme-tags#include) | [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/include.js) | [Test][tt] +`layout, block` | [Document](http://docs.mixture.io/templates/) | [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/layout.js) | [Test][tt] ## Filters diff --git a/index.js b/index.js index cc4153acd..f57f46b80 100644 --- a/index.js +++ b/index.js @@ -58,7 +58,7 @@ var _engine = { filepath += this.options.extname; } var tpl = this.options.cache && this.cache[filepath] || - this.parse(fs.readFileSync(filepath)); + this.parse(fs.readFileSync(filepath, 'utf8')); return this.options.cache ? (this.cache[filepath] = tpl) : tpl; } }; diff --git a/package.json b/package.json index c085f8d1e..05aa1d6f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-liquid", - "version": "1.1.4", + "version": "1.1.5", "description": "A Shopify Liquid Implementation in Node.js", "main": "index.js", "scripts": { diff --git a/src/scope.js b/src/scope.js index 3ca39a35c..75b43f91c 100644 --- a/src/scope.js +++ b/src/scope.js @@ -8,7 +8,6 @@ var scope = { var v = _.get(this.scopes[i], str); if (v !== undefined) return v; } - return ''; }, set: function(k, v) { _.set(this.scopes[this.scopes.length - 1], k, v); diff --git a/tags/layout.js b/tags/layout.js new file mode 100644 index 000000000..bd776c9ce --- /dev/null +++ b/tags/layout.js @@ -0,0 +1,51 @@ +var Liquid = require('..'); +var lexical = Liquid.lexical; +var withRE = new RegExp(`with\\s+(${lexical.value.source})`); + +module.exports = function(liquid) { + + liquid.registerTag('layout', { + parse: function(token, remainTokens){ + var match = lexical.value.exec(token.args); + if(!match) error(`illegal token ${token.raw}`, token); + + this.layout = match[0]; + this.tpls = liquid.parser.parse(remainTokens); + }, + render: function(scope, hash) { + var layout = Liquid.evalValue(this.layout, scope); + var tpl = liquid.handleCache(layout); + + scope.push({}); + liquid.renderer.renderTemplates(this.tpls, scope); + var html = liquid.renderer.renderTemplates(tpl, scope); + scope.pop(); + return html; + } + }); + + liquid.registerTag('block', { + parse: function(token, remainTokens){ + var match = /\w+/.exec(token.args); + this.block = match ? match[0] : ''; + + this.tpls = []; + var p, stream = liquid.parser.parseStream(remainTokens) + .on('tag:endblock', token => stream.stop()) + .on('template', tpl => this.tpls.push(tpl)) + .on('end', x => { + throw new Error(`tag ${token.raw} not closed`); + }); + stream.start(); + }, + render: function(scope, hash){ + var html = scope.get(`_liquid.blocks.${this.block}`); + if(html === undefined){ + html = liquid.renderer.renderTemplates(this.tpls, scope); + } + scope.set(`_liquid.blocks.${this.block}`, html); + return html; + } + }); + +}; diff --git a/test/expression.js b/test/expression.js index e37c8435f..436f016c4 100644 --- a/test/expression.js +++ b/test/expression.js @@ -40,7 +40,7 @@ describe('expression', function() { expect(evalExp('one<=two', scope)).to.equal(true); expect(evalExp('x contains "x"', scope)).to.equal(false); expect(evalExp('x contains "X"', scope)).to.equal(true); - expect(evalExp('x contains z', scope)).to.equal(true); + expect(evalExp('x contains z', scope)).to.equal(false); expect(evalExp('"<=" == "<="', scope)).to.equal(true); }); diff --git a/test/scope.js b/test/scope.js index 5f09b4f4d..d2a19d3f4 100644 --- a/test/scope.js +++ b/test/scope.js @@ -44,7 +44,7 @@ describe('scope', function() { scope.push({foo: 'foo', foo1: 'foo1'}); scope.pop(); expect(scope.get('foo')).to.equal('bar'); - expect(scope.get('foo1')).to.equal(''); + expect(scope.get('foo1')).to.equal(undefined); expect(scope.get('bar[1].b[1]')).to.equal(2); }); }); diff --git a/test/tags.js b/test/tags.js index 11deb4775..da8eaf67a 100644 --- a/test/tags.js +++ b/test/tags.js @@ -31,6 +31,9 @@ describe('tags', function() { emptyArray: [] }; mock({ + '/default-layout.html': 'foo{% block %}Default{% endblock %}foo', + '/multi-blocks-layout.html': 'foo{% block "a"%}{% endblock %}{% block b%}{%endblock%}foo', + '/multi-blocks.html': '{% layout "multi-blocks-layout" %}{%block a%}aaa{%endblock%},{%block b%};{%block c%}ccc{%endblock%};{%endblock%}', '/files/foo.html': 'foo', '/current.html': 'bar{% include "foo.html" %}bar', '/relative.html': 'bar{% include "../files/foo.html" %}bar', @@ -202,6 +205,11 @@ describe('tags', function() { 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/); + }); + it('should support tablerow with range', function() { src = '{% tablerow i in (1..5) cols:2 %}{{ i }}{% endtablerow %}'; dst = '