diff --git a/README.md b/README.md index 2259eeb7c..d6e05a342 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ shall be implemented. Documentation: - [x] [assign](https://shopify.github.io/liquid/tags/control-flow/) -- [ ] [case/when](https://shopify.github.io/liquid/tags/control-flow/) +- [x] [case/when](https://shopify.github.io/liquid/tags/control-flow/) - [ ] [if](https://shopify.github.io/liquid/tags/control-flow/) - [ ] [unless](https://shopify.github.io/liquid/tags/control-flow/) - [ ] [elsif/else](https://shopify.github.io/liquid/tags/control-flow/) diff --git a/filter.js b/filter.js index 59359dddd..f670e9152 100644 --- a/filter.js +++ b/filter.js @@ -12,7 +12,7 @@ var _filterInstance = { module.exports = function() { var filters = {}; - function parse(str) { + function construct(str) { var match = lexical.patterns.filterLine.exec(str.trim()); if (!match) { throw new Error('illegal filter: ' + str); @@ -43,6 +43,6 @@ module.exports = function() { } return { - parse, register, clear + construct, register, clear }; }; diff --git a/index.js b/index.js index a1fcd96b8..fea1487f1 100644 --- a/index.js +++ b/index.js @@ -24,11 +24,12 @@ function factory(){ engine.tag = Tag(); engine.filter = Filter(); - var render = Render(engine.filter, engine.tag); + var renderer = Render(engine.filter, engine.tag); + engine.evaluate = renderer.evaluate; + engine.renderTokens = renderer.render; engine.render = function(html, ctx) { - return render.render(tokenizer.parse(html), context.factory(ctx)); + return engine.renderTokens(tokenizer.parse(html), context.factory(ctx)); }, - engine.evaluate = render.evaluate; fs.readdirSync(tagsPath).map(function(f){ var match = /^(\w+)\.js$/.exec(f); diff --git a/lexical.js b/lexical.js index 613be6bf5..85f9f7bbd 100644 --- a/lexical.js +++ b/lexical.js @@ -25,7 +25,7 @@ var rangeLine = new RegExp(`^(?:${range.source})$`); var filterLine = new RegExp(`^(?:${filter.source})$`); exports.patterns = { - quoted, number, bool, range, literal, hash, filter, identifier, + quoted, number, bool, range, literal, hash, filter, identifier, value, quotedLine, numberLine, boolLine, rangeLine, literalLine, filterLine }; diff --git a/render.js b/render.js index 29e1b6ff2..6850ac86d 100644 --- a/render.js +++ b/render.js @@ -21,22 +21,23 @@ module.exports = function(Filter, Tag) { } function evaluate(str, ctx) { + if(!ctx) throw new Error('ctx is needed to evaluate'); var filters = str.split('|'); var val = ctx.get(filters.shift()); return filters - .map(str => Filter.parse(str)) + .map(str => Filter.construct(str)) .reduce((v, filter) => filter.render(v, ctx), val); } function renderTag(token, tokens, ctx) { - var tag = Tag.parse(token.value), + var tag = Tag.construct(token), subTokens = []; if (tag.needClose) { var curToken, endToken = 'end' + tag.name; - while ((curToken = tokens.shift()).value !== endToken) { + while ((curToken = tokens.shift()) && curToken.value !== endToken) { subTokens.push(curToken); } - if (curToken.value !== endToken) { + if (!curToken) { throw new Error(`${token.value} not closed`); } } diff --git a/tag.js b/tag.js index 9d356c355..9edcba0b8 100644 --- a/tag.js +++ b/tag.js @@ -30,22 +30,15 @@ module.exports = function() { tags[name] = tag; } - function parse(str) { - var match = lexical.patterns.identifier.exec(str.trim()); - if (!match) throw new Error('illegal tag: ' + str); - - var tagInstance = factory(match[0], str); - return tagInstance; - } - - function factory(name, markup) { - var tag = tags[name]; - if (!tag) throw new Error(`tag ${name} not found`); + function construct(token) { + var tag = tags[token.name]; + if (!tag) throw new Error(`tag ${token.name} not found`); var instance = Object.create(_tagInstance); - instance.name = name; - instance.markup = markup; + instance.name = token.name; + instance.markup = token.value; instance.tag = tag; + instance.needClose = tag.needClose; return instance; } @@ -54,6 +47,6 @@ module.exports = function() { } return { - parse, register, hash, clear + construct, register, hash, clear }; }; diff --git a/tags/case.js b/tags/case.js new file mode 100644 index 000000000..0f8bdd1c8 --- /dev/null +++ b/tags/case.js @@ -0,0 +1,34 @@ +var Liquid = require('..'); +var patterns = Liquid.lexical.patterns; +var caseRE = new RegExp(`^\\s*case\\s+(${patterns.value.source})`); +var whenRE = new RegExp(`^\\s*when\\s+(${patterns.value.source})`); + +module.exports = function(liquid) { + + liquid.registerTag('case', { + needClose: true, + render: function(tokens, ctx, markup, hash) { + var match = markup.match(caseRE); + var cond = liquid.evaluate(match[1], ctx); + + var partialTokens = [], + matching = false; + for (var i = 0; i < tokens.length; i++) { + var token = tokens[i]; + if (token.type === 'tag' && token.name === 'when') { + if (matching) break; + match = token.value.match(whenRE); + if(!match) continue; + + var val = liquid.evaluate(match[1], ctx); + if (val === cond) matching = true; + } else if (token.type === 'tag' && token.name === 'else') { + if (matching) break; + else matching = true; + } else if (matching) partialTokens.push(token); + } + + return liquid.renderTokens(partialTokens, ctx); + } + }); +}; diff --git a/test/filter.js b/test/filter.js index e79433dee..1e6fb1839 100644 --- a/test/filter.js +++ b/test/filter.js @@ -16,19 +16,19 @@ describe('filter', function() { }); it('should throw when not registered', function() { expect(function() { - filter.parse('foo'); + filter.construct('foo'); }).to.throw(/filter foo not found/); }); it('should register a simple filter', function(){ filter.register('foo', x => x.toUpperCase()); - expect(filter.parse('foo').render('foo', ctx)).to.equal('FOO'); + expect(filter.construct('foo').render('foo', ctx)).to.equal('FOO'); }); it('should call filter with corrct arguments', function(){ var spy = sinon.spy(); filter.register('foo', spy); - filter.parse('foo: 33').render('foo', ctx); + filter.construct('foo: 33').render('foo', ctx); expect(spy).to.have.been.calledWith('foo', 33); }); }); diff --git a/test/render.js b/test/render.js index 94b023480..eebd1850d 100644 --- a/test/render.js +++ b/test/render.js @@ -22,7 +22,8 @@ describe('render', function() { }); tagToken = { type: 'tag', - value: 'foo bar:x foo:"FOO" num:2.3' + value: 'foo bar:x foo:"FOO" num:2.3', + name: 'foo' }; htmlToken = { type: 'html', diff --git a/test/tag.js b/test/tag.js index a56057b43..c7e83189c 100644 --- a/test/tag.js +++ b/test/tag.js @@ -9,7 +9,7 @@ var context = require('../context.js'); describe('tag', function() { var ctx; - before(function(){ + before(function() { ctx = context.factory({ foo: 'bar', arr: [2, 1] @@ -19,7 +19,11 @@ describe('tag', function() { it('should throw when not registered', function() { expect(function() { - tag.parse('foo'); + tag.construct({ + type: 'tag', + value: 'foo', + name: 'foo' + }); }).to.throw(/tag foo not found/); }); @@ -44,7 +48,11 @@ describe('tag', function() { tag.register('foo', { render: spy }); - tag.parse('foo').render(tokens, ctx); + tag.construct({ + type: 'tag', + value: 'foo', + name: 'foo' + }).render(tokens, ctx); expect(spy).to.have.been.called; }); @@ -54,7 +62,11 @@ describe('tag', function() { tag.register('foo', { render: spy }); - var t = tag.parse('foo aa:foo bb: arr[0] cc: 2.3'); + var t = tag.construct({ + type: 'tag', + value: 'foo aa:foo bb: arr[0] cc: 2.3', + name: 'foo' + }); t.render(tokens, ctx); expect(spy).to.have.been.calledWithMatch(tokens, ctx, 'foo', { aa: 'bar', diff --git a/test/tags.js b/test/tags.js index e8ec39aac..8aeae51c6 100644 --- a/test/tags.js +++ b/test/tags.js @@ -4,16 +4,31 @@ const expect = chai.expect; var liquid = require('..')(); var ctx = { + empty: '', foo: 'bar', arr: [-2, 'a'] }; describe('tags', function() { it('should support assign', function() { - test('{% assign foo="bar"%}{{foo}}', 'bar'); + expect(liquid.render('{% assign foo="bar"%}{{foo}}', ctx)).to.equal('bar'); + }); + it('should support case', function() { + expect(function() { + liquid.render('{% case "foo"%}'); + }).to.throw(/case "foo" not closed/); + expect(liquid.render('{% case "foo"%}' + + '{% when "foo" %}foo{% when "bar"%}bar' + + '{%endcase%}', ctx)).to.equal('foo'); + expect(liquid.render('{% case empty %}' + + '{% when "foo" %}foo{% when ""%}bar' + + '{%endcase%}')).to.equal('bar'); + expect(liquid.render('{% case false %}' + + '{% when "foo" %}foo{% when ""%}bar' + + '{%endcase%}')).to.equal(''); + expect(liquid.render('{% case "a" %}' + + '{% when "b" %}b{% when "c"%}c{%else %}d' + + '{%endcase%}')).to.equal('d'); }); }); -function test(src, dst){ - expect(liquid.render(src, ctx)).to.equal(dst); -} diff --git a/tokenizer.js b/tokenizer.js index e4ea0e013..350cbbe61 100644 --- a/tokenizer.js +++ b/tokenizer.js @@ -1,3 +1,4 @@ +const lexical = require('./lexical.js'); function parse(html){ var tokens = []; @@ -12,21 +13,21 @@ function parse(html){ htmlFragment = html.slice(idx, result.index); tokens.push({ type: 'html', - raw: htmlFragment, value: htmlFragment }); } if(result[1]){ + var match = result[1].trim().match(lexical.patterns.identifier); + if (!match) throw new Error('illegal tag: ' + result[1]); tokens.push({ type: 'tag', - raw: result[1], + name: match[0], value: result[2].trim() }); } else{ tokens.push({ type: 'output', - raw: result[3], value: result[4].trim() }); } @@ -37,7 +38,6 @@ function parse(html){ htmlFragment = html.slice(idx, html.length); tokens.push({ type: 'html', - raw: htmlFragment, value: htmlFragment }); }