diff --git a/lexical.js b/lexical.js index 4900915a3..7a981883d 100644 --- a/lexical.js +++ b/lexical.js @@ -6,12 +6,12 @@ var doubleQuoted = /"[^"]*"/; var quoteBalanced = new RegExp(`(?:${singleQuoted.source}|${doubleQuoted.source}|[^'"])*`); var number = /(?:-?\d+\.?\d*|\.?\d+)/; -var bool = /true|false/i; +var bool = /true|false/; var identifier = /[a-zA-Z_$][a-zA-Z_$0-9]*/; var subscript = /\[\d+\]/; var quoted = new RegExp(`(?:${singleQuoted.source}|${doubleQuoted.source})`); -var literal = new RegExp(`(?:${quoted.source}|${bool.source}|${number.source})`, 'i'); +var literal = new RegExp(`(?:${quoted.source}|${bool.source}|${number.source})`); var variable = new RegExp(`${identifier.source}(?:\\.${identifier.source}|${subscript.source})*`); // range related @@ -19,10 +19,10 @@ var rangeLimit = new RegExp(`(?:${variable.source}|${number.source})`); var range = new RegExp(`\\(${rangeLimit.source}\\.\\.${rangeLimit.source}\\)`); var rangeCapture = new RegExp(`\\((${rangeLimit.source})\\.\\.(${rangeLimit.source})\\)`); -var value = new RegExp(`(?:${literal.source}|${variable.source}|${range.source})`, 'i'); +var value = new RegExp(`(?:${literal.source}|${variable.source}|${range.source})`); // hash related -var hash = new RegExp(`(?:${identifier.source})\\s*:\\s*(?:${value.source})`, 'g'); +var hash = new RegExp(`(?:${identifier.source})\\s*:\\s*(?:${value.source})`); var hashCapture = new RegExp(`(${identifier.source})\\s*:\\s*(${value.source})`, 'g'); var tagLine = new RegExp(`^\\s*(${identifier.source})\\s*(.*)\\s*$`); diff --git a/tag.js b/tag.js index 2b1a45af3..d927ceb4c 100644 --- a/tag.js +++ b/tag.js @@ -2,21 +2,6 @@ const lexical = require('./lexical.js'); const Exp = require('./expression.js'); const TokenizationError = require('./error.js').TokenizationError; -var _tagInstance = { - render: function(scope, register) { - var reg = register[this.name]; - if(!reg) reg = register[this.name] = {}; - var obj = hash(this.token.args, scope); - return this.tagImpl.render(scope, obj, reg) || ''; - }, - parse: function(tokens){ - if(this.tagImpl.parse){ - this.tagImpl.parse(this.token, tokens); - } - return this; - } -}; - function hash(markup, scope) { var obj = {}; lexical.hashCapture.lastIndex = 0; @@ -31,6 +16,27 @@ function hash(markup, scope) { module.exports = function() { var tagImpls = {}; + var _tagInstance = { + render: function(scope, register) { + var reg = register[this.name]; + if(!reg) reg = register[this.name] = {}; + var obj = hash(this.token.args, scope); + return this.tagImpl.render(scope, obj, reg) || ''; + }, + parse: function(token, tokens){ + this.type = 'tag'; + this.token = token; + this.name = token.name; + + var tagImpl = tagImpls[this.name]; + if (!tagImpl) throw new Error(`tag ${this.name} not found`); + this.tagImpl = Object.create(tagImpl); + if(this.tagImpl.parse){ + this.tagImpl.parse(token, tokens); + } + } + }; + function register(name, tag) { if (typeof tag.render !== 'function') { throw new Error(`expect ${name}.render to be a function`); @@ -38,15 +44,9 @@ module.exports = function() { tagImpls[name] = tag; } - function construct(token) { - var tagImpl = tagImpls[token.name]; - if (!tagImpl) throw new Error(`tag ${token.name} not found`); - + function construct(token, tokens) { var instance = Object.create(_tagInstance); - instance.token = token; - instance.type = 'tag'; - instance.name = token.name; - instance.tagImpl = Object.create(tagImpl); + instance.parse(token, tokens); return instance; } diff --git a/tags/cycle.js b/tags/cycle.js index c4eb08466..3ad73a606 100644 --- a/tags/cycle.js +++ b/tags/cycle.js @@ -20,7 +20,7 @@ module.exports = function(liquid) { } if (!this.candidates.length){ - throw new Error(`illegal tag: ${tagToken.raw}`); + throw new Error(`empty candidates: ${tagToken.raw}`); } }, diff --git a/tags/unless.js b/tags/unless.js index c1bfe5c18..988bed864 100644 --- a/tags/unless.js +++ b/tags/unless.js @@ -3,23 +3,11 @@ var lexical = Liquid.lexical; module.exports = function(liquid) { liquid.registerTag('unless', { - parse: function(tagToken, remainTokens) { - this.branches = []; - this.elseTemplates = []; - var p, stream = liquid.parseStream(remainTokens) - .onStart(x => this.branches.push({ - cond: tagToken.args, - templates: p = [] - })) - .onTag('elsif', token => { - if (!this.branches[token.args]) { - this.branches.push({ - cond: token.args, - templates: p = [] - }); - } + .onStart(x => { + p = this.templates = []; + this.cond = tagToken.args; }) .onTag('else', token => this.elseTemplates = p = []) .onTag('endunless', token => stream.stop()) @@ -32,17 +20,10 @@ module.exports = function(liquid) { }, render: function(scope, hash) { - for (var i = 0; i < this.branches.length; i++) { - var branch = this.branches[i]; - var cond = Liquid.evalExp(branch.cond, scope); - cond = Liquid.isTruthy(cond); - if (i === 0) cond = !cond; - if (cond) { - return liquid.renderTemplates(branch.templates, scope); - } - } - return liquid.renderTemplates(this.elseTemplates, scope); + var cond = Liquid.evalExp(this.cond, scope); + return Liquid.isFalsy(cond) ? + liquid.renderTemplates(this.templates, scope) : + liquid.renderTemplates(this.elseTemplates, scope); } - }); }; diff --git a/template.js b/template.js index 05574ef9d..957a29b09 100644 --- a/template.js +++ b/template.js @@ -77,7 +77,7 @@ module.exports = function(Tag, Filter) { function parseTag(token, tokens) { if (token.name === 'continue' || token.name === 'break') return token; - return Tag.construct(token).parse(tokens); + return Tag.construct(token, tokens); } function parseOutput(str) { diff --git a/test/tag.js b/test/tag.js index 0a1a54928..834ec574a 100644 --- a/test/tag.js +++ b/test/tag.js @@ -23,7 +23,7 @@ describe('tag', function() { type: 'tag', value: 'foo', name: 'foo' - }); + }, []); }).to.throw(/tag foo not found/); }); @@ -52,7 +52,7 @@ describe('tag', function() { type: 'tag', value: 'foo', name: 'foo' - }).render(scope, {}); + }, []).render(scope, {}); expect(spy).to.have.been.called; }); @@ -68,7 +68,7 @@ describe('tag', function() { name: 'foo', args: 'aa:foo bb: arr[0] cc: 2.3' }; - tag.construct(token).render(scope, {}); + tag.construct(token, []).render(scope, {}); expect(spy).to.have.been.calledWithMatch(scope, { aa: 'bar', bb: 2, diff --git a/test/tags.js b/test/tags.js index eec807fbd..c190d2f37 100644 --- a/test/tags.js +++ b/test/tags.js @@ -27,11 +27,13 @@ describe('tags', function() { emptyArray: [] }; }); + 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 case', function() { testThrow('{% case "foo"%}', /{% case "foo"%} not closed/); test('{% case "foo"%}' + @@ -69,10 +71,22 @@ describe('tags', function() { testThrow('{% capture = %}{%endcapture%}', /= not valid identifier/); }); + it('should throw when for capture closed', function() { + testThrow('{%capture c%}{{c}}', /tag .* not closed/); + }); + it('should support for', function() { 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/); + }); + + it('should support for else', function() { + test('{%for c in ""%}a{%else%}b{%endfor%}', 'b'); + }); + it('should support for with forloop', function() { src = '{%for c in alpha%}' + '{{forloop.first}}.{{forloop.index}}.{{forloop.index0}}.' + @@ -117,6 +131,10 @@ describe('tags', function() { test(src + src + src + src, '1231'); }); + it('should throw when cycle candidates empty', function() { + 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');