diff --git a/README.md b/README.md index 882b8b2e3..2eee7ff53 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,8 @@ Documentation: - [ ] 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] -- [ ] increment [Document](https://shopify.github.io/liquid/tags/variable/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt] -- [ ] decrement [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] ## Filters diff --git a/error.js b/error.js new file mode 100644 index 000000000..f4ba888ce --- /dev/null +++ b/error.js @@ -0,0 +1,11 @@ + +function factory(msg, token){ + var err = new Error(msg || 'unkown error'); + if(token){ + err.token = token.raw; + err.line = token.line; + } + throw err; +} + +module.exports = factory; diff --git a/filter.js b/filter.js index bb3eaf652..08626eec1 100644 --- a/filter.js +++ b/filter.js @@ -14,12 +14,9 @@ module.exports = function() { function construct(str) { var match = lexical.filterLine.exec(str.trim()); - if (!match) { - throw new Error('illegal filter: ' + str); - } - var k = match[1], - v = match[2]; + if (!match) error('illegal filter: ' + str); + var k = match[1], v = match[2]; return factory(k, [v]); } diff --git a/index.js b/index.js index db2a104cf..e96e2d68f 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,7 @@ const path = require("path"); const fs = require('fs'); const Tag = require('./tag.js'); const Filter = require('./filter.js'); +const error = require('./error.js'); const tagsPath = path.join(__dirname, "tags"); const filtersPath = path.join(__dirname, "filters"); @@ -45,5 +46,6 @@ function factory(){ } factory.lexical = lexical; +factory.error = error; module.exports = factory; diff --git a/package.json b/package.json index a89b6e25e..36ad548f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-liquid", - "version": "1.0.1", + "version": "1.0.2", "description": "A Shopify Liquid Implementation in Node.js", "main": "index.js", "scripts": { @@ -21,7 +21,8 @@ }, "homepage": "https://github.com/harttle/shopify-liquid#readme", "dependencies": { - "lodash": "^4.13.1" + "lodash": "^4.13.1", + "strftime": "^0.9.2" }, "devDependencies": { "chai": "^3.5.0", diff --git a/render.js b/render.js index feed49490..731f3260e 100644 --- a/render.js +++ b/render.js @@ -1,5 +1,6 @@ const lexical = require('./lexical.js'); const syntax = require('./syntax.js'); +const error = require('./error.js'); module.exports = function(Filter, Tag) { function render(tokens, scope) { @@ -17,14 +18,14 @@ module.exports = function(Filter, Tag) { html += renderTag(token, tokens, scope); break; default: - throw new Error(`unexpected type: ${token.type}`); + error(`unexpected type: ${token.type}`, token); } } return html; } function evalExp(exp, scope) { - if(!scope) throw new Error('unable to evalExp: scope undefined'); + if(!scope) error('unable to evalExp: scope undefined'); var operatorREs = lexical.operators; for (var i = 0; i < operatorREs.length; i++) { var operatorRE = operatorREs[i]; @@ -41,7 +42,7 @@ module.exports = function(Filter, Tag) { } function evalFilter(str, scope){ - if(!scope) throw new Error('unable to evalFilter: scope undefined'); + if(!scope) error('unable to evalFilter: scope undefined'); var filters = str.split('|'); var val = scope.get(filters.shift()); return filters @@ -57,9 +58,7 @@ module.exports = function(Filter, Tag) { while ((curToken = tokens.shift()) && curToken.value !== endToken) { subTokens.push(curToken); } - if (!curToken) { - throw new Error(`${token.value} not closed`); - } + if (!curToken) error(`${token.value} not closed`); } return tag.render(subTokens, scope); } diff --git a/tags/assign.js b/tags/assign.js index 1d485d1c3..f31a32771 100644 --- a/tags/assign.js +++ b/tags/assign.js @@ -6,7 +6,7 @@ module.exports = function(liquid) { liquid.registerTag('assign', { render: function(tokens, scope, token, hash) { - var match = token.value.match(re); + var match = token.args.match(re); var k = match[1], v = match[2]; scope.set(k, liquid.evaluate(v, scope)); } diff --git a/tags/decrement.js b/tags/decrement.js new file mode 100644 index 000000000..e8eea2ee6 --- /dev/null +++ b/tags/decrement.js @@ -0,0 +1,17 @@ +const Liquid = require('..'); +const lexical = Liquid.lexical; +const error = Liquid.error; + +module.exports = function(liquid) { + + liquid.registerTag('decrement', { + render: function(tokens, scope, token, hash) { + 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); + } + }); + +}; diff --git a/tags/increment.js b/tags/increment.js new file mode 100644 index 000000000..f3bdeb6b6 --- /dev/null +++ b/tags/increment.js @@ -0,0 +1,17 @@ +const Liquid = require('..'); +const lexical = Liquid.lexical; +const error = Liquid.error; + +module.exports = function(liquid) { + + liquid.registerTag('increment', { + render: function(tokens, scope, token, hash) { + 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); + } + }); + +}; diff --git a/test/tags.js b/test/tags.js index 88062370c..111cd8e8d 100644 --- a/test/tags.js +++ b/test/tags.js @@ -1,18 +1,20 @@ const chai = require("chai"); const expect = chai.expect; -var liquid = require('..')(); - -var ctx = { - one: 1, - two: 2, - leq: '<=', - empty: '', - foo: 'bar', - arr: [-2, 'a'] -}; +var liquid = require('..')(), + ctx; describe('tags', function() { + beforeEach(function() { + ctx = { + one: 1, + two: 2, + leq: '<=', + empty: '', + foo: 'bar', + arr: [-2, 'a'] + }; + }); it('should support assign', function() { expect(liquid.render('{% assign foo="bar"%}{{foo}}', ctx)).to.equal('bar'); }); @@ -34,22 +36,31 @@ describe('tags', function() { '{%endcase%}')).to.equal('d'); }); - it('should support if', function(){ + it('should support if', function() { expect(liquid.render('{% if 2==3 %}yes{%else%}no{%endif%}', ctx)).to.equal('no'); expect(liquid.render('{% if 1==2 and one2 %}yes{%endunless%}', ctx)).to.equal('yes'); }); - it('should support capture', function(){ + it('should support capture', function() { expect(liquid.render('{% capture f %}{{"a" | capitalize}}{%endcapture%}{{f}}', ctx)).to.equal('A'); - expect(function(){ + expect(function() { liquid.render('{% capture = %}{%endcapture%}', ctx); }).to.throw(/= not valid identifier/); }); -}); + it('should support increment', function() { + expect(liquid.render('{% increment foo %}{%increment foo%}{{foo}}', ctx)).to.equal('2'); + expect(liquid.render('{% increment one %}{{one}}', ctx)).to.equal('2'); + }); + + it('should support decrement', function() { + expect(liquid.render('{% decrement foo %}{%decrement foo%}{{foo}}', ctx)).to.equal('-2'); + expect(liquid.render('{% decrement one %}{{one}}', ctx)).to.equal('0'); + }); +}); diff --git a/tokenizer.js b/tokenizer.js index 36157201c..d4d30de1d 100644 --- a/tokenizer.js +++ b/tokenizer.js @@ -1,11 +1,12 @@ const lexical = require('./lexical.js'); +const error = require('./error.js'); function parse(html){ var tokens = []; if(!html) return tokens; var syntax = /({%(.*?)%})|({{(.*?)}})/g; - var result, htmlFragment; + var result, htmlFragment, token; var idx = 0; while ((result = syntax.exec(html)) !== null) { @@ -16,29 +17,19 @@ function parse(html){ value: htmlFragment }); } - var rawTag = result[1], - cleanTag = result[2], - rawOut = result[3], - cleanOut = result[4]; - if(rawTag){ - var match = cleanTag.trim().match(lexical.tagLine); - if (!match) throw new Error('illegal tag: ' + rawTag); - var name = match[1], args = match[2]; + if(result[1]){ + token = factory('tag', 1, result); + var match = token.value.match(lexical.tagLine); + if (!match) error('illegal tag', token); - tokens.push({ - type: 'tag', - value: cleanTag.trim(), - raw: rawTag, - args, name - }); + token.name = match[1]; + token.args = match[2]; + tokens.push(token); } else{ - tokens.push({ - type: 'output', - raw: rawOut, - value: cleanOut.trim() - }); + token = factory('output', 3, result); + tokens.push(token); } idx = syntax.lastIndex; } @@ -51,6 +42,24 @@ function parse(html){ }); } return tokens; + + function factory(type, offset, match){ + var token = { + type, + raw: match[offset], + value: match[offset + 1].trim(), + line: crCount(match.index) + 1 + }; + return token; + } + + var lastIdx = -1; + var lastCount = 0; + function crCount(idx){ + lastCount += html.slice(lastIdx+1, idx); + lastIdx = idx; + return lastCount; + } } exports.parse = parse;