From 0b7da2abf79ee7aae61f39b45a3ca378db8de3a8 Mon Sep 17 00:00:00 2001 From: harttle Date: Fri, 17 Jun 2016 08:39:23 +0800 Subject: [PATCH] fix existing tags new tags: for,cycle --- README.md | 18 +++---- expression.js | 54 ++++++++++++++++++++ filter.js | 3 +- index.js | 10 ++-- lexical.js | 31 +++++++----- package.json | 2 +- render.js | 48 +++++++----------- scope.js | 21 +++----- tag.js | 18 ++++--- tags/assign.js | 10 ++-- tags/case.js | 57 ++++++++++++--------- tags/cycle.js | 43 ++++++++++++++++ tags/decrement.js | 13 +++-- tags/for.js | 42 ++++++++++++++-- tags/if.js | 3 +- tags/increment.js | 13 +++-- tags/unless.js | 2 +- template.js | 7 ++- test/expression.js | 60 ++++++++++++++++++++++ test/lexical.js | 17 +++---- test/render.js | 19 ------- test/scope.js | 11 ++--- test/tag.js | 4 +- test/tags.js | 121 ++++++++++++++++++++++++++++++++------------- 24 files changed, 432 insertions(+), 195 deletions(-) create mode 100644 expression.js create mode 100644 tags/cycle.js create mode 100644 test/expression.js diff --git a/README.md b/README.md index 61fb0c061..774e4bb88 100644 --- a/README.md +++ b/README.md @@ -25,18 +25,18 @@ Documentation: - [x] if [Document](https://shopify.github.io/liquid/tags/control-flow/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/if.js) [Test][tt] - [x] unless [Document](https://shopify.github.io/liquid/tags/control-flow/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/unless.js) [Test][tt] - [x] elsif [Document/else](https://shopify.github.io/liquid/tags/control-flow/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/if.js) [Test][tt] -- [ ] for [Document](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/for.js) [Test][tt] -- [ ] break [Document](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt] -- [ ] continue [Document](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt] -- [ ] for [Document: limit,offset,range,reversed)](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt] -- [ ] cycle [Document](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt] -- [ ] cycle [Document: group)](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt] +- [x] for [Document](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/for.js) [Test][tt] +- [x] break [Document](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/for.js) [Test][tt] +- [x] continue [Document](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/for.js) [Test][tt] +- [x] for [Document: limit,offset,range,reversed)](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/for.js) [Test][tt] +- [x] cycle [Document](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/cycle.js) [Test][tt] +- [x] cycle [Document: group)](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/cycle.js) [Test][tt] - [ ] tablerow [Document](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt] - [ ] tablerow [Document: cols,limit,offset,range](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt] - [x] assign [Document](https://shopify.github.io/liquid/tags/variable/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/assign.js) [Test][tt] -- [x] capture [Document](https://shopify.github.io/liquid/tags/variable/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt] -- [x] increment [Document](https://shopify.github.io/liquid/tags/variable/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt] -- [x] decrement [Document](https://shopify.github.io/liquid/tags/variable/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt] +- [x] capture [Document](https://shopify.github.io/liquid/tags/variable/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/capture.js) [Test][tt] +- [x] increment [Document](https://shopify.github.io/liquid/tags/variable/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/increment.js) [Test][tt] +- [x] decrement [Document](https://shopify.github.io/liquid/tags/variable/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/decrement.js) [Test][tt] ## Filters diff --git a/expression.js b/expression.js new file mode 100644 index 000000000..06c9b4787 --- /dev/null +++ b/expression.js @@ -0,0 +1,54 @@ +const syntax = require('./syntax.js'); +const Exp = require('./expression.js'); +const lexical = require('./lexical.js'); +const _ = require('lodash'); + +function evalExp(exp, scope) { + if (!scope) throw new Error('unable to evalExp: scope undefined'); + var operatorREs = lexical.operators; + for (var i = 0; i < operatorREs.length; i++) { + var operatorRE = operatorREs[i]; + var expRE = new RegExp(`^(${lexical.quoteBalanced.source})(${operatorRE.source})(${lexical.quoteBalanced.source})$`); + var match = exp.match(expRE); + if (match) { + var l = evalExp(match[1], scope); + var op = syntax.operators[match[2].trim()]; + var r = evalExp(match[3], scope); + return op(l, r); + } + } + + if (match = exp.match(lexical.rangeLine)) { + var low = evalValue(match[1], scope), + high = evalValue(match[2], scope) + 1; + return _.range(low, high); + } + + return evalValue(exp, scope); +} + +function evalValue(str, scope) { + str = str && str.trim(); + if (!str) return undefined; + + if (lexical.isLiteral(str)) { + var a = lexical.parseLiteral(str); + return lexical.parseLiteral(str); + } + if (lexical.isVariable(str)) { + return scope.get(str); + } +} + +function isTruthy(val) { + if (val instanceof Array) return !!val.length; + return !!val; +} + +function isFalsy(val) { + return !isTruthy(val); +} + +module.exports = { + evalExp, evalValue, isTruthy, isFalsy +}; diff --git a/filter.js b/filter.js index 08626eec1..29dc78a3f 100644 --- a/filter.js +++ b/filter.js @@ -1,9 +1,10 @@ const lexical = require('./lexical.js'); const _ = require('lodash'); +const Exp = require('./expression.js'); var _filterInstance = { render: function(output, scope) { - var args = this.args.map(arg => scope.get(arg)); + var args = this.args.map(arg => Exp.evalValue(arg, scope)); args.unshift(output); return this.filter.apply(null, args); } diff --git a/index.js b/index.js index 3429a50bb..2f3f81c56 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,7 @@ const Tag = require('./tag.js'); const Filter = require('./filter.js'); const error = require('./error.js'); const Template = require('./template'); +const Expression = require('./expression.js'); const tagsPath = path.join(__dirname, "tags"); @@ -33,14 +34,13 @@ function factory(){ engine.parseStream = template.parseStream; var renderer = Render(engine.filter, engine.tag); - engine.evaluate = renderer.evalExp; - engine.evalExp = renderer.evalExp; engine.evalFilter = renderer.evalFilter; engine.renderTemplates = renderer.renderTemplates; engine.render = function(html, ctx) { var tokens = engine.tokenize(html); var templates = engine.parse(tokens); + engine.register = {}; return engine.renderTemplates(templates, scope.factory(ctx)); }, @@ -55,7 +55,11 @@ function factory(){ factory.lexical = lexical; factory.error = error; -factory.isTruthy = Render.isTruthy; +factory.isTruthy = Expression.isTruthy; +factory.isFalsy = Expression.isFalsy; factory.stringify = Render.stringify; +factory.evalExp = Expression.evalExp; +factory.evalValue = Expression.evalValue; + module.exports = factory; diff --git a/lexical.js b/lexical.js index 2917290e5..39c9028b5 100644 --- a/lexical.js +++ b/lexical.js @@ -5,15 +5,18 @@ var doubleQuoted = /"[^"]*"/; var number = /-?\d+\.?\d*|\.?\d+/; var bool = /true|false/i; -var range = /\((\d+)\.\.(\d+)\)/; 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}|${range.source}|${bool.source}|${number.source}`, 'i'); +var literal = new RegExp(`${quoted.source}|${bool.source}|${number.source}`, 'i'); var variable = new RegExp(`${identifier.source}(?:\\.${identifier.source}|${subscript.source})*`); var value = new RegExp(`${literal.source}|${variable.source}`, 'i'); -var hash = new RegExp(`(${identifier.source})\\s*:\\s*(${value.source})`, 'g'); +var range = new RegExp(`\\((?:${value.source})\\.\\.(?:${value.source})\\)`); +var rangeCapture = new RegExp(`\\((${value.source})\\.\\.(${value.source})\\)`); +var variableOrRange = new RegExp(`${variable.source}|${range.source}`); +var hash = new RegExp(`(?:${identifier.source})\\s*:\\s*(?:${value.source})`, 'g'); +var hashCapture = new RegExp(`(${identifier.source})\\s*:\\s*(${value.source})`, 'g'); var filter = new RegExp(`(${identifier.source})(?:\\s*:\\s*(${value.source}))?`); var quoteBalanced = new RegExp(`(?:${singleQuoted.source}|${doubleQuoted.source}|[^'"])*`); @@ -23,7 +26,7 @@ var variableLine = new RegExp(`^(?:${variable.source})$`); var numberLine = new RegExp(`^(?:${number.source})$`); var boolLine = new RegExp(`^(?:${bool.source})$`, 'i'); var quotedLine = new RegExp(`^(?:${quoted.source})$`); -var rangeLine = new RegExp(`^(?:${range.source})$`); +var rangeLine = new RegExp(`^(?:${rangeCapture.source})$`); var filterLine = new RegExp(`^(?:${filter.source})$`); var operators = [ @@ -33,15 +36,19 @@ var operators = [ ]; -function isLiteral (str) { +function isLiteral(str) { return literalLine.test(str); } -function isVariable (str) { +function isRange(str) { + return rangeLine.test(str); +} + +function isVariable(str) { return variableLine.test(str); } -function parseLiteral (str) { +function parseLiteral(str) { var res; if (res = str.match(numberLine)) { return Number(str); @@ -52,15 +59,13 @@ function parseLiteral (str) { if (res = str.match(quotedLine)) { return str.slice(1, -1); } - if (res = str.match(rangeLine)) { - return _.range(res[1], res[2]); - } } module.exports = { - quoted, number, bool, range, literal, hash, filter, + quoted, number, bool, literal, filter, + hash, hashCapture, + range, rangeCapture, variableOrRange, identifier, value, quoteBalanced, operators, quotedLine, numberLine, boolLine, rangeLine, literalLine, filterLine, tagLine, - isLiteral, isVariable, parseLiteral + isLiteral, isVariable, parseLiteral, isRange }; - diff --git a/package.json b/package.json index 36ad548f9..73ca0cc83 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-liquid", - "version": "1.0.2", + "version": "1.0.3", "description": "A Shopify Liquid Implementation in Node.js", "main": "index.js", "scripts": { diff --git a/render.js b/render.js index d356dca18..149da932b 100644 --- a/render.js +++ b/render.js @@ -1,66 +1,52 @@ const lexical = require('./lexical.js'); -const syntax = require('./syntax.js'); const error = require('./error.js'); +const Exp = require('./expression.js'); function stringify(val) { if (typeof val === 'string') return val; return JSON.stringify(val); } -function isTruthy(val) { - if (val instanceof Array) return !!val.length; - return !!val; -} - function factory(Filter, Tag) { function renderTemplates(templates, scope) { var html = ''; - var template; - while (template = templates.shift()) { + templates.some(template => { + if (scope.get('forloop.skip')) return true; if (template.type === 'tag') { - html += template.render(scope); + if (template.name === 'continue') { + scope.set('forloop.skip', true); + return true; + } + if (template.name === 'break') { + scope.set('forloop.stop', true); + scope.set('forloop.skip', true); + return true; + } + html += template.render(scope, this.register); } else if (template.type === 'html') { html += template.value; } else if (template.type === 'output') { var val = evalFilter(template.value, scope); html += stringify(val); } - } + }); return html; } - function evalExp(exp, scope) { - if (!scope) error('unable to evalExp: scope undefined'); - var operatorREs = lexical.operators; - for (var i = 0; i < operatorREs.length; i++) { - var operatorRE = operatorREs[i]; - var expRE = new RegExp(`^(${lexical.quoteBalanced.source})(${operatorRE.source})(${lexical.quoteBalanced.source})$`); - var match = exp.match(expRE); - if (match) { - var l = evalExp(match[1], scope); - var op = syntax.operators[match[2].trim()]; - var r = evalExp(match[3], scope); - return op(l, r); - } - } - return evalFilter(exp, scope); - } - function evalFilter(str, scope) { - if (!scope) error('unable to evalFilter: scope undefined'); + if (!scope) throw new Error('unable to evalFilter: scope undefined'); var filters = str.split('|'); - var val = scope.get(filters.shift()); + var val = Exp.evalValue(filters.shift(), scope); return filters .map(str => Filter.construct(str)) .reduce((v, filter) => filter.render(v, scope), val); } return { - renderTemplates, evalFilter, evalExp + renderTemplates, evalFilter }; }; -factory.isTruthy = isTruthy; factory.stringify = stringify; module.exports = factory; diff --git a/scope.js b/scope.js index 49f17155d..3ca39a35c 100644 --- a/scope.js +++ b/scope.js @@ -4,27 +4,18 @@ const error = require('./error.js'); var scope = { get: function(str) { - str = str && str.trim(); - if(!str) return ''; - - if (lexical.isLiteral(str)) { - var a = lexical.parseLiteral(str); - return lexical.parseLiteral(str); - } - if (lexical.isVariable(str)) { - for (var i = this.scopes.length - 1; i >= 0; i--) { - var v = _.get(this.scopes[i], str); - if (v !== undefined) return v; - } + for (var i = this.scopes.length - 1; i >= 0; i--) { + var v = _.get(this.scopes[i], str); + if (v !== undefined) return v; } return ''; }, - set: function(k, v){ - this.scopes[this.scopes.length-1][k] = v; + set: function(k, v) { + _.set(this.scopes[this.scopes.length - 1], k, v); return this; }, push: function(ctx) { - if(!ctx) error(`trying to push ${ctx} into scopes`); + if (!ctx) error(`trying to push ${ctx} into scopes`); return this.scopes.push(ctx); }, pop: function() { diff --git a/tag.js b/tag.js index 1f1b6b932..685b4c85f 100644 --- a/tag.js +++ b/tag.js @@ -1,24 +1,28 @@ const lexical = require('./lexical.js'); +const Exp = require('./expression.js'); var _tagInstance = { - render: function(scope) { + 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) || ''; + return this.tagImpl.render(scope, obj, reg) || ''; }, parse: function(tokens){ - this.tagImpl.parse(this.token, tokens); + if(this.tagImpl.parse){ + this.tagImpl.parse(this.token, tokens); + } return this; } }; function hash(markup, scope) { var obj = {}; - lexical.hash.lastIndex = 0; - while (match = lexical.hash.exec(markup)) { + lexical.hashCapture.lastIndex = 0; + while (match = lexical.hashCapture.exec(markup)) { var k = match[1], v = match[2]; - if (!k) continue; - obj[k] = scope.get(v); + obj[k] = Exp.evalValue(v, scope); } return obj; } diff --git a/tags/assign.js b/tags/assign.js index f31a32771..d03e97a08 100644 --- a/tags/assign.js +++ b/tags/assign.js @@ -5,10 +5,14 @@ var re = new RegExp(`(${lexical.identifier.source})\\s*=(.*)`); module.exports = function(liquid) { liquid.registerTag('assign', { - render: function(tokens, scope, token, hash) { + parse: function(token){ var match = token.args.match(re); - var k = match[1], v = match[2]; - scope.set(k, liquid.evaluate(v, scope)); + if(!match) error(`illegal token ${token.raw}`, token); + this.key = match[1]; + this.value = match[2]; + }, + render: function(scope, hash) { + scope.set(this.key, Liquid.evalValue(this.value, scope)); } }); diff --git a/tags/case.js b/tags/case.js index 09df829de..c8d0c8d0a 100644 --- a/tags/case.js +++ b/tags/case.js @@ -1,34 +1,45 @@ var Liquid = require('..'); var lexical = Liquid.lexical; -var caseRE = new RegExp(`^\\s*case\\s+(${lexical.value.source})`); -var whenRE = new RegExp(`^\\s*when\\s+(${lexical.value.source})`); module.exports = function(liquid) { - liquid.registerTag('case', { - needClose: true, - render: function(tokens, scope, token, hash) { - var match = token.value.match(caseRE); - var cond = liquid.evaluate(match[1], scope); - var partialTokens = [], - matching = false; - for (var i = 0; i < tokens.length; i++) { - var tk = tokens[i]; - if (tk.type === 'tag' && tk.name === 'when') { - if (matching) break; - match = tk.value.match(whenRE); - if(!match) continue; + parse: function(tagToken, remainTokens) { + this.cond = tagToken.args; + this.cases = []; + this.elseTemplates = []; - var val = liquid.evaluate(match[1], scope); - if (val === cond) matching = true; - } else if (tk.type === 'tag' && tk.name === 'else') { - if (matching) break; - else matching = true; - } else if (matching) partialTokens.push(tk); + var p = [], + stream = liquid.parseStream(remainTokens) + .onTag('when', token => { + if (!this.cases[token.args]) { + this.cases.push({ + val: token.args, + templates: p = [] + }); + } + }) + .onTag('else', token => p = this.elseTemplates) + .onTag('endcase', token => stream.stop()) + .onTemplate(tpl => p.push(tpl)) + .onEnd(x => { + throw new Error(`tag ${tagToken.raw} not closed`); + }); + + stream.start(); + }, + + render: function(scope, hash) { + for (var i = 0; i < this.cases.length; i++) { + var branch = this.cases[i]; + var val = Liquid.evalExp(branch.val, scope); + var cond = Liquid.evalExp(this.cond, scope); + if (val === cond) { + return liquid.renderTemplates(branch.templates, scope); + } } - - return liquid.renderTokens(partialTokens, scope); + return liquid.renderTemplates(this.elseTemplates, scope); } + }); }; diff --git a/tags/cycle.js b/tags/cycle.js new file mode 100644 index 000000000..c4eb08466 --- /dev/null +++ b/tags/cycle.js @@ -0,0 +1,43 @@ +var Liquid = require('..'); +var lexical = Liquid.lexical; +var groupRE = new RegExp(`^(?:(${lexical.value.source})\\s*:\\s*)?(.*)$`); +var candidatesRE = new RegExp(lexical.value.source, 'g'); + +module.exports = function(liquid) { + liquid.registerTag('cycle', { + + parse: function(tagToken, remainTokens) { + var match = groupRE.exec(tagToken.args); + if(!match) throw new Error(`illegal tag: ${tagToken.raw}`); + + this.group = match[1] || ''; + var candidates = match[2]; + + this.candidates = []; + + while(match = candidatesRE.exec(candidates)){ + this.candidates.push(match[0]); + } + + if (!this.candidates.length){ + throw new Error(`illegal tag: ${tagToken.raw}`); + } + }, + + render: function(scope, hash, register) { + var fingerprint = Liquid.evalValue(this.group, scope) + ':' + + this.candidates.join(','); + var idx = register[fingerprint]; + + if(idx === undefined){ + idx = register[fingerprint] = 0; + } + + var candidate = this.candidates[idx]; + idx = (idx + 1) % this.candidates.length; + register[fingerprint] = idx; + + return Liquid.evalValue(candidate, scope); + } + }); +}; diff --git a/tags/decrement.js b/tags/decrement.js index e8eea2ee6..69b64bf43 100644 --- a/tags/decrement.js +++ b/tags/decrement.js @@ -5,12 +5,15 @@ const error = Liquid.error; module.exports = function(liquid) { liquid.registerTag('decrement', { - render: function(tokens, scope, token, hash) { + parse: function(token) { var match = token.args.match(lexical.identifier); - if(!match) error(`illegal identifier ${token.args}`, token); - var k = match[0], v = scope.get(k); - if(typeof v !== 'number') v = 0; - scope.set(k, v-1); + if (!match) error(`illegal identifier ${token.args}`, token); + this.variable = match[0]; + }, + render: function(scope, hash) { + var v = scope.get(this.variable); + if (typeof v !== 'number') v = 0; + scope.set(this.variable, v - 1); } }); diff --git a/tags/for.js b/tags/for.js index 6dfe1267a..961187a0f 100644 --- a/tags/for.js +++ b/tags/for.js @@ -1,15 +1,19 @@ var Liquid = require('..'); var lexical = Liquid.lexical; -var re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+(.+)$`); +var re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+` + + `(${lexical.variableOrRange.source})` + + `(?:\\s+${lexical.hash.source})*` + + `(?:\\s+(reversed))?$`); module.exports = function(liquid) { liquid.registerTag('for', { parse: function(tagToken, remainTokens) { var match = re.exec(tagToken.args); - if(!match) throw new Error(`illegal tag: ${tagToken.raw}`); + if (!match) throw new Error(`illegal tag: ${tagToken.raw}`); this.variable = match[1]; this.collection = match[2]; + this.reversed = !!match[3]; this.templates = []; this.elseTemplates = []; @@ -27,12 +31,40 @@ module.exports = function(liquid) { }, render: function(scope, hash) { - var collection = liquid.evaluate(this.collection, scope); - if(!(collection instanceof Array) || !collection.length){ + var collection = Liquid.evalExp(this.collection, scope); + if (Liquid.isFalsy(collection)) { return liquid.renderTemplates(this.elseTemplates, scope); } - liquid.renderTemplates(this.templates, scope); + var html = '', + ctx = {}, + length = collection.length; + var offset = hash.offset || 0; + var limit = (hash.limit === undefined) ? collection.length : hash.limit; + + var collection = collection.slice(offset, offset + limit); + if(this.reversed) collection.reverse(); + collection.some((item, i) => { + ctx[this.variable] = item; + ctx.forloop = { + first: i === 0, + index: i + 1, + index0: i, + last: i === length - 1, + length: length, + rindex: length - i, + rindex0: length - i - 1, + stop: false, + skip: false + }; + scope.push(ctx); + html += liquid.renderTemplates(this.templates, scope); + var breakloop = scope.get('forloop.stop'); + scope.pop(ctx); + + if (breakloop) return true; + }); + return html; } }); }; diff --git a/tags/if.js b/tags/if.js index 1956550c2..1dbe85776 100644 --- a/tags/if.js +++ b/tags/if.js @@ -5,6 +5,7 @@ module.exports = function(liquid) { liquid.registerTag('if', { parse: function(tagToken, remainTokens) { + this.branches = []; this.elseTemplates = []; @@ -34,7 +35,7 @@ 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.evaluate(branch.cond, scope); + var cond = Liquid.evalExp(branch.cond, scope); if (Liquid.isTruthy(cond)) { return liquid.renderTemplates(branch.templates, scope); } diff --git a/tags/increment.js b/tags/increment.js index f3bdeb6b6..3f1eb816a 100644 --- a/tags/increment.js +++ b/tags/increment.js @@ -5,12 +5,15 @@ const error = Liquid.error; module.exports = function(liquid) { liquid.registerTag('increment', { - render: function(tokens, scope, token, hash) { + parse: function(token) { var match = token.args.match(lexical.identifier); - if(!match) error(`illegal identifier ${token.args}`, token); - var k = match[0], v = scope.get(k); - if(typeof v !== 'number') v = 0; - scope.set(k, v+1); + if (!match) error(`illegal identifier ${token.args}`, token); + this.variable = match[0]; + }, + render: function(scope, hash) { + var v = scope.get(this.variable); + if (typeof v !== 'number') v = 0; + scope.set(this.variable, v + 1); } }); diff --git a/tags/unless.js b/tags/unless.js index d327565e4..c1bfe5c18 100644 --- a/tags/unless.js +++ b/tags/unless.js @@ -34,7 +34,7 @@ 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.evaluate(branch.cond, scope); + var cond = Liquid.evalExp(branch.cond, scope); cond = Liquid.isTruthy(cond); if (i === 0) cond = !cond; if (cond) { diff --git a/template.js b/template.js index c97c8f7af..40f350c59 100644 --- a/template.js +++ b/template.js @@ -26,7 +26,12 @@ module.exports = function(Tag) { var template; if (token.type == 'tag') { if (this.trigger(`tag:${token.name}`, token)) continue; - template = parseTag(token, this.tokens); + if (token.name === 'continue' || token.name === 'break'){ + template = token; + } + else{ + template = parseTag(token, this.tokens); + } } else { template = token; } diff --git a/test/expression.js b/test/expression.js new file mode 100644 index 000000000..0994c1ecc --- /dev/null +++ b/test/expression.js @@ -0,0 +1,60 @@ +const chai = require("chai"); +const sinonChai = require("sinon-chai"); +const sinon = require("sinon"); +const expect = chai.expect; + +chai.use(sinonChai); + +var expression = require('../expression.js'); +var Scope = require('../scope.js'); +var evalExp = expression.evalExp; +var evalValue = expression.evalValue; + +describe('expression', function() { + var scope; + + beforeEach(function() { + scope = Scope.factory({ + one: 1, + two: 2, + x: 'XXX' + }); + }); + + it('should eval literals', function() { + expect(evalValue('2.3')).to.equal(2.3); + expect(evalValue('"foo"')).to.equal("foo"); + }); + + it('should throw on illegal expression', function() { + expect(function() { + evalExp('1 contains "x"', scope); + }).to.throw(); + }); + + it('should eval variables', function() { + expect(evalValue('23', scope)).to.equal(23); + expect(evalValue('one', scope)).to.equal(1); + expect(evalValue('x', scope)).to.equal('XXX'); + }); + + it('should eval simple expression', function() { + expect(evalExp('1<2', scope)).to.equal(true); + expect(evalExp('2<=2', scope)).to.equal(true); + 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('"<=" == "<="', scope)).to.equal(true); + }); + + it('should eval complex expression', function() { + expect(evalExp('1<2 and x contains "x"', scope)).to.equal(false); + expect(evalExp('1<2 or x contains "x"', scope)).to.equal(true); + }); + + it("should eval range expression", function() { + expect(evalExp('(2..4)', scope)).to.deep.equal([2,3,4]); + expect(evalExp('(two..4)', scope)).to.deep.equal([2,3,4]); + }); +}); diff --git a/test/lexical.js b/test/lexical.js index 63ab624ed..8f1a74733 100644 --- a/test/lexical.js +++ b/test/lexical.js @@ -17,6 +17,12 @@ describe('identifier', function() { identifier.isLiteral('23').should.equal(true); }); + it("should test range literal", function() { + identifier.isRange("(12..32)").should.equal(true); + identifier.isRange("(12..foo)").should.equal(true); + identifier.isRange("(foo.bar..foo)").should.equal(true); + }); + it('should test string literal', function() { identifier.isLiteral('""').should.equal(true); identifier.isLiteral('"a\'b"').should.equal(true); @@ -24,10 +30,6 @@ describe('identifier', function() { identifier.isLiteral("'a bcd'").should.equal(true); }); - it("should test range literal", function() { - identifier.isLiteral("(12..32)").should.equal(true); - }); - it("should test variable", function() { identifier.isVariable("foo").should.equal(true); identifier.isVariable("foo.bar.foo").should.equal(true); @@ -65,11 +67,4 @@ describe('identifier', function() { identifier.parseLiteral('"ab\'c"').should.equal("ab\'c"); }); - it("should parse range literal", function() { - var arr = identifier.parseLiteral("(12..32)"); - arr.length.should.equal(20); - arr[0].should.equal(12); - arr[1].should.equal(13); - arr[19].should.equal(31); - }); }); diff --git a/test/render.js b/test/render.js index 7497dc713..eb0be6a2d 100644 --- a/test/render.js +++ b/test/render.js @@ -10,7 +10,6 @@ var Scope = require('../scope.js'); var filter = require('../filter')(); var Render = require('../render.js')(filter, tag); var render = Render.renderTemplates; -var evalExp = Render.evalExp; var evalFilter = Render.evalFilter; describe('render', function() { @@ -18,9 +17,6 @@ describe('render', function() { beforeEach(function() { scope = Scope.factory({ - one: 1, - two: 2, - x: 'XXX', foo: { bar: ['a', 2] } @@ -51,19 +47,4 @@ describe('render', function() { filter.register('time', (l, r) => l + 3 * r); expect(evalFilter('foo.bar[0] | date: "b" | time:2', scope)).to.equal('ab6'); }); - - it('should eval expression', function() { - expect(evalExp('1<2', scope)).to.equal(true); - expect(evalExp('2<=2', scope)).to.equal(true); - expect(evalExp('one<=two', scope)).to.equal(true); - expect(function() { - evalExp('1 contains "x"', scope); - }).to.throw(); - 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('1<2 and x contains "x"', scope)).to.equal(false); - expect(evalExp('1<2 or x contains "x"', scope)).to.equal(true); - expect(evalExp('"<=" == "<="', scope)).to.equal(true); - }); }); diff --git a/test/scope.js b/test/scope.js index 08021b00f..693b80ab2 100644 --- a/test/scope.js +++ b/test/scope.js @@ -13,12 +13,6 @@ describe('scope', function() { }); }); - it('should parse literal', function() { - scope.get('2.3').should.equal(2.3); - scope.get('(2..4)').should.deep.equal([2,3]); - scope.get('"foo"').should.equal("foo"); - }); - it('should get property', function() { scope.get('foo').should.equal('bar'); }); @@ -28,6 +22,11 @@ describe('scope', function() { scope.get('foo').should.equal('FOO'); }); + it('should set child property', function() { + scope.set('oo.bar', 'FOO'); + scope.get('oo.bar').should.equal('FOO'); + }); + it('should get desendent property', function() { scope.get('bar[0]').should.equal('a'); scope.get('bar[1].b').should.deep.equal([1, 2]); diff --git a/test/tag.js b/test/tag.js index ce69a9a0a..0a1a54928 100644 --- a/test/tag.js +++ b/test/tag.js @@ -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 e024faa64..21259a9be 100644 --- a/test/tags.js +++ b/test/tags.js @@ -2,7 +2,7 @@ const chai = require("chai"); const expect = chai.expect; var liquid = require('..')(), - ctx; + ctx, src, dst; function test(src, dst) { expect(liquid.render(src, ctx)).to.equal(dst); @@ -22,30 +22,29 @@ describe('tags', function() { leq: '<=', empty: '', foo: 'bar', - arr: [-2, 'a', { - foo: 'bar' - }], + arr: [-2, 'a'], + alpha: ['a', 'b', 'c'], emptyArray: [] }; }); - //it('should support assign', function() { - //test('{% assign foo="bar"%}{{foo}}', 'bar'); - //}); - //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 assign', function() { + test('{% assign foo="bar"%}{{foo}}', 'bar'); + }); + 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 if', function() { testThrow('{% if false%}yes', /tag {% if false%} not closed/); @@ -68,17 +67,73 @@ describe('tags', function() { testThrow('{% capture = %}{%endcapture%}', /= not valid identifier/); }); - //it('should support for', function() { - //test('{%for i in arr%}{{"a" | capitalize}}{%endcapture%}{{f}}', 'A'); - //}); + it('should support for', function() { + test('{%for c in alpha%}{{c}}{%endfor%}', 'abc'); + }); - //it('should support increment', function() { - //test('{% increment foo %}{%increment foo%}{{foo}}', '2'); - //test('{% increment one %}{{one}}', '2'); - //}); + it('should support for with forloop', function() { + src = '{%for c in alpha%}' + + '{{forloop.first}}.{{forloop.index}}.{{forloop.index0}}.' + + '{{forloop.last}}.{{forloop.length}}.' + + '{{forloop.rindex}}.{{forloop.rindex0}}' + + '{{c}}\n' + + '{%endfor%}'; + 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); + }); - //it('should support decrement', function() { - //test('{% decrement foo %}{%decrement foo%}{{foo}}', '-2'); - //test('{% decrement one %}{{one}}', '0'); - //}); + it('should support for with continue and break', function() { + src = '{% for i in (1..5) %}' + + '{% if i == 4 %}{% continue %}' + + '{% else %}{{ i }}' + + '{% endif %}' + + '{% endfor %}'; + test(src, '1235'); + src = '{% for i in (one..5) %}' + + '{% if i == 4 %}{% break %}{% endif %}' + + '{{ i }}' + + '{% endfor %}'; + test(src, '123'); + }); + + it('should support for with limit and offset', function() { + src = '{% for i in (1..5) limit:2 %}{{ i }}{% endfor %}'; + test(src, '12'); + src = '{% for i in (1..10) limit:2 offset:5%}{{ i }}{% endfor %}'; + test(src, '67'); + }); + + it('should support for reversed', function() { + src = '{% for i in (1..5) limit:2 reversed %}{{ i }}{% endfor %}'; + test(src, '21'); + }); + + it('should support cycle', function() { + src = "{% cycle '1', '2', '3' %}"; + test(src + src + src + src, '1231'); + }); + + it('should support cycle in for block', function() { + src = '{% for i in (1..5) %}{% cycle one, "e"%}{% endfor %}'; + 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'); + }); + + it('should support increment', function() { + test('{% increment foo %}{%increment foo%}{{foo}}', '2'); + test('{% increment one %}{{one}}', '2'); + }); + + it('should support decrement', function() { + test('{% decrement foo %}{%decrement foo%}{{foo}}', '-2'); + test('{% decrement one %}{{one}}', '0'); + }); });