From 58daab6128ee861f4b81967ac6445d871f927458 Mon Sep 17 00:00:00 2001 From: harttle Date: Wed, 15 Jun 2016 22:37:24 +0800 Subject: [PATCH] filters --- README.md | 123 ++++++++++++++++++++++------------------- filter.js | 6 +- filters.js | 13 +++++ filters/abs.js | 3 - index.js | 17 +++--- lexical.js | 30 +++++++--- package.json | 2 +- render.js | 48 +++++++++++----- context.js => scope.js | 16 +++--- syntax.js | 13 +++++ tag.js | 17 +++--- tags/assign.js | 10 ++-- tags/case.js | 26 ++++----- tags/if.js | 24 ++++++++ tags/unless.js | 21 +++++++ test/context.js | 51 ----------------- test/filter.js | 10 ++-- test/filters.js | 39 ++++++++++++- test/render.js | 43 ++++++++++---- test/scope.js | 51 +++++++++++++++++ test/tag.js | 19 ++++--- test/tags.js | 14 +++++ tokenizer.js | 21 +++++-- 23 files changed, 402 insertions(+), 215 deletions(-) create mode 100644 filters.js delete mode 100644 filters/abs.js rename context.js => scope.js (65%) create mode 100644 syntax.js create mode 100644 tags/if.js create mode 100644 tags/unless.js delete mode 100644 test/context.js create mode 100644 test/scope.js diff --git a/README.md b/README.md index d6e05a342..2ffcef6ee 100644 --- a/README.md +++ b/README.md @@ -11,74 +11,79 @@ shall be implemented. > Shopify liquid is used by [Jekyll][jekyll] and [Github Pages][gh]. +## Operators + +Documentation: + +`==`, `!=`, `>`, `<`, `>=`, `<=`, `or`, `and`, `contains`. + ## Tags Documentation: -- [x] [assign](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/) -- [ ] [for](https://shopify.github.io/liquid/tags/iteration/) -- [ ] [break](https://shopify.github.io/liquid/tags/iteration/) -- [ ] [continue](https://shopify.github.io/liquid/tags/iteration/) -- [ ] [for: limit,offset,range,reversed)](https://shopify.github.io/liquid/tags/iteration/) -- [ ] [cycle](https://shopify.github.io/liquid/tags/iteration/) -- [ ] [cycle: group)](https://shopify.github.io/liquid/tags/iteration/) -- [ ] [tablerow](https://shopify.github.io/liquid/tags/iteration/) -- [ ] [tablerow: cols,limit,offset,range](https://shopify.github.io/liquid/tags/iteration/) -- [ ] [assign](https://shopify.github.io/liquid/tags/variable/) -- [ ] [capture](https://shopify.github.io/liquid/tags/variable/) -- [ ] [increment](https://shopify.github.io/liquid/tags/variable/) -- [ ] [decrement](https://shopify.github.io/liquid/tags/variable/) +- [x] case/when [Document](https://shopify.github.io/liquid/tags/control-flow/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/case.js) [Test][tt] +- [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] +- [ ] 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] +- [ ] 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] ## Filters Documentation: -- [x] [abs](https://shopify.github.io/liquid/filters/abs/) -- [ ] [append](https://shopify.github.io/liquid/filters/append) -- [ ] [capitalize](https://shopify.github.io/liquid/filters/capitalize) -- [ ] [ceil](https://shopify.github.io/liquid/filters/ceil) -- [ ] [date](https://shopify.github.io/liquid/filters/date) -- [ ] [default](https://shopify.github.io/liquid/filters/default) -- [ ] [divided_by](https://shopify.github.io/liquid/filters/divided_by) -- [ ] [downcase](https://shopify.github.io/liquid/filters/downcase) -- [ ] [escape](https://shopify.github.io/liquid/filters/escape) -- [ ] [escape_once](https://shopify.github.io/liquid/filters/escape_once) -- [ ] [first](https://shopify.github.io/liquid/filters/first) -- [ ] [floor](https://shopify.github.io/liquid/filters/floor) -- [ ] [join](https://shopify.github.io/liquid/filters/join) -- [ ] [last](https://shopify.github.io/liquid/filters/last) -- [ ] [lstrip](https://shopify.github.io/liquid/filters/lstrip) -- [ ] [map](https://shopify.github.io/liquid/filters/map) -- [ ] [minus](https://shopify.github.io/liquid/filters/minus) -- [ ] [modulo](https://shopify.github.io/liquid/filters/modulo) -- [ ] [newline_to_br](https://shopify.github.io/liquid/filters/newline_to_br) -- [ ] [plus](https://shopify.github.io/liquid/filters/plus) -- [ ] [prepend](https://shopify.github.io/liquid/filters/prepend) -- [ ] [remove](https://shopify.github.io/liquid/filters/remove) -- [ ] [remove_first](https://shopify.github.io/liquid/filters/remove_first) -- [ ] [replace](https://shopify.github.io/liquid/filters/replace) -- [ ] [replace_first](https://shopify.github.io/liquid/filters/replace_first) -- [ ] [reverse](https://shopify.github.io/liquid/filters/reverse) -- [ ] [round](https://shopify.github.io/liquid/filters/round) -- [ ] [rstrip](https://shopify.github.io/liquid/filters/rstrip) -- [ ] [size](https://shopify.github.io/liquid/filters/size) -- [ ] [slice](https://shopify.github.io/liquid/filters/slice) -- [ ] [sort](https://shopify.github.io/liquid/filters/sort) -- [ ] [split](https://shopify.github.io/liquid/filters/split) -- [ ] [strip](https://shopify.github.io/liquid/filters/strip) -- [ ] [strip_html](https://shopify.github.io/liquid/filters/strip_html) -- [ ] [strip_newlines](https://shopify.github.io/liquid/filters/strip_newlines) -- [ ] [times](https://shopify.github.io/liquid/filters/times) -- [ ] [truncate](https://shopify.github.io/liquid/filters/truncate) -- [ ] [truncatewords](https://shopify.github.io/liquid/filters/truncatewords) -- [ ] [uniq](https://shopify.github.io/liquid/filters/uniq) -- [ ] [upcase](https://shopify.github.io/liquid/filters/upcase) -- [ ] [url_encode](https://shopify.github.io/liquid/filters/url_encode) +- [x] abs [Document](https://shopify.github.io/liquid/filters/abs/) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] append [Document](https://shopify.github.io/liquid/filters/append) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] capitalize [Document](https://shopify.github.io/liquid/filters/capitalize) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] ceil [Document](https://shopify.github.io/liquid/filters/ceil) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] date [Document](https://shopify.github.io/liquid/filters/date) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] default [Document](https://shopify.github.io/liquid/filters/default) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] divided_by [Document](https://shopify.github.io/liquid/filters/divided_by) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] downcase [Document](https://shopify.github.io/liquid/filters/downcase) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] escape [Document](https://shopify.github.io/liquid/filters/escape) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] escape_once [Document](https://shopify.github.io/liquid/filters/escape_once) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] first [Document](https://shopify.github.io/liquid/filters/first) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] floor [Document](https://shopify.github.io/liquid/filters/floor) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] join [Document](https://shopify.github.io/liquid/filters/join) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] last [Document](https://shopify.github.io/liquid/filters/last) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] lstrip [Document](https://shopify.github.io/liquid/filters/lstrip) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] map [Document](https://shopify.github.io/liquid/filters/map) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] minus [Document](https://shopify.github.io/liquid/filters/minus) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] modulo [Document](https://shopify.github.io/liquid/filters/modulo) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] newline_to_br [Document](https://shopify.github.io/liquid/filters/newline_to_br) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] plus [Document](https://shopify.github.io/liquid/filters/plus) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] prepend [Document](https://shopify.github.io/liquid/filters/prepend) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] remove [Document](https://shopify.github.io/liquid/filters/remove) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] remove_first [Document](https://shopify.github.io/liquid/filters/remove_first) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] replace [Document](https://shopify.github.io/liquid/filters/replace) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] replace_first [Document](https://shopify.github.io/liquid/filters/replace_first) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] reverse [Document](https://shopify.github.io/liquid/filters/reverse) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] round [Document](https://shopify.github.io/liquid/filters/round) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] rstrip [Document](https://shopify.github.io/liquid/filters/rstrip) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] size [Document](https://shopify.github.io/liquid/filters/size) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] slice [Document](https://shopify.github.io/liquid/filters/slice) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] sort [Document](https://shopify.github.io/liquid/filters/sort) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] split [Document](https://shopify.github.io/liquid/filters/split) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] strip [Document](https://shopify.github.io/liquid/filters/strip) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] strip_html [Document](https://shopify.github.io/liquid/filters/strip_html) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] strip_newlines [Document](https://shopify.github.io/liquid/filters/strip_newlines) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] times [Document](https://shopify.github.io/liquid/filters/times) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] truncate [Document](https://shopify.github.io/liquid/filters/truncate) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] truncatewords [Document](https://shopify.github.io/liquid/filters/truncatewords) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] uniq [Document](https://shopify.github.io/liquid/filters/uniq) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] upcase [Document](https://shopify.github.io/liquid/filters/upcase) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [ ] url_encode [Document](https://shopify.github.io/liquid/filters/url_encode) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] ## Async Support @@ -97,3 +102,5 @@ For template-driven projects, checkout these Liquid-like engines: [shopify-liquid]: https://shopify.github.io/liquid/ [jekyll]: http://jekyllrb.com/ [gh]: https://pages.github.com/ +[tt]: https://github.com/harttle/shopify-liquid/blob/master/test/tags.js +[ft]: https://github.com/harttle/shopify-liquid/blob/master/test/filters.js diff --git a/filter.js b/filter.js index f670e9152..bb3eaf652 100644 --- a/filter.js +++ b/filter.js @@ -2,8 +2,8 @@ const lexical = require('./lexical.js'); const _ = require('lodash'); var _filterInstance = { - render: function(output, ctx) { - var args = this.args.map(arg => ctx.get(arg)); + render: function(output, scope) { + var args = this.args.map(arg => scope.get(arg)); args.unshift(output); return this.filter.apply(null, args); } @@ -13,7 +13,7 @@ module.exports = function() { var filters = {}; function construct(str) { - var match = lexical.patterns.filterLine.exec(str.trim()); + var match = lexical.filterLine.exec(str.trim()); if (!match) { throw new Error('illegal filter: ' + str); } diff --git a/filters.js b/filters.js new file mode 100644 index 000000000..361dfbb7e --- /dev/null +++ b/filters.js @@ -0,0 +1,13 @@ +const _ = require('lodash'); +const strftime = require('strftime').timezone(-(new Date()).getTimezoneOffset()); + +module.exports = function(liquid) { + liquid.registerFilter('abs', v => Math.abs(v)); + liquid.registerFilter('append', (v, arg) => v + arg); + liquid.registerFilter('capitalize', v => _.capitalize(v)); + liquid.registerFilter('ceil', v => Math.ceil(v)); + liquid.registerFilter('date', (v, arg) => strftime(arg, v)); + liquid.registerFilter('default', (v, arg) => arg || v); + liquid.registerFilter('divided_by', (v, arg) => Math.floor(v/arg)); + liquid.registerFilter('downcase', v => v.toLowerCase()); +}; diff --git a/filters/abs.js b/filters/abs.js deleted file mode 100644 index a7a10cf57..000000000 --- a/filters/abs.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function(liquid) { - liquid.registerFilter('abs', v => Math.abs(v)); -}; diff --git a/index.js b/index.js index fea1487f1..db2a104cf 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ -const context = require('./context'); +const scope = require('./scope'); const tokenizer = require('./tokenizer.js'); const Render = require('./render.js'); const lexical = require('./lexical.js'); @@ -25,10 +25,14 @@ function factory(){ engine.filter = Filter(); var renderer = Render(engine.filter, engine.tag); - engine.evaluate = renderer.evaluate; + + engine.evaluate = renderer.evalExp; + engine.evalExp = renderer.evalExp; + engine.evalFilter = renderer.evalFilter; + engine.renderTokens = renderer.render; engine.render = function(html, ctx) { - return engine.renderTokens(tokenizer.parse(html), context.factory(ctx)); + return engine.renderTokens(tokenizer.parse(html), scope.factory(ctx)); }, fs.readdirSync(tagsPath).map(function(f){ @@ -36,12 +40,7 @@ function factory(){ if(!match) return; require("./tags/" + f)(engine); }); - - fs.readdirSync(filtersPath).map(function(f){ - var match = /^(\w+)\.js$/.exec(f); - if(!match) return; - require("./filters/" + f)(engine); - }); + require("./filters.js")(engine); return engine; } diff --git a/lexical.js b/lexical.js index 85f9f7bbd..2917290e5 100644 --- a/lexical.js +++ b/lexical.js @@ -15,7 +15,9 @@ var variable = new RegExp(`${identifier.source}(?:\\.${identifier.source}|${subs var value = new RegExp(`${literal.source}|${variable.source}`, 'i'); var hash = 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}|[^'"])*`); +var tagLine = new RegExp(`^\\s*(${identifier.source})\\s*(.*)\\s*$`); var literalLine = new RegExp(`^(?:${literal.source})$`, 'i'); var variableLine = new RegExp(`^(?:${variable.source})$`); var numberLine = new RegExp(`^(?:${number.source})$`); @@ -24,20 +26,22 @@ var quotedLine = new RegExp(`^(?:${quoted.source})$`); var rangeLine = new RegExp(`^(?:${range.source})$`); var filterLine = new RegExp(`^(?:${filter.source})$`); -exports.patterns = { - quoted, number, bool, range, literal, hash, filter, identifier, value, - quotedLine, numberLine, boolLine, rangeLine, literalLine, filterLine -}; +var operators = [ + /\s+or\s+/, + /\s+and\s+/, + /==|!=|<=|>=|<|>|\s+contains\s+/ +]; -exports.isLiteral = function(str) { + +function isLiteral (str) { return literalLine.test(str); -}; +} -exports.isVariable = function(str) { +function isVariable (str) { return variableLine.test(str); -}; +} -exports.parseLiteral = function(str) { +function parseLiteral (str) { var res; if (res = str.match(numberLine)) { return Number(str); @@ -51,4 +55,12 @@ exports.parseLiteral = function(str) { if (res = str.match(rangeLine)) { return _.range(res[1], res[2]); } +} + +module.exports = { + quoted, number, bool, range, literal, hash, filter, + identifier, value, quoteBalanced, operators, + quotedLine, numberLine, boolLine, rangeLine, literalLine, filterLine, tagLine, + isLiteral, isVariable, parseLiteral }; + diff --git a/package.json b/package.json index 43fb96321..a89b6e25e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-liquid", - "version": "1.0.0", + "version": "1.0.1", "description": "A Shopify Liquid Implementation in Node.js", "main": "index.js", "scripts": { diff --git a/render.js b/render.js index 6850ac86d..feed49490 100644 --- a/render.js +++ b/render.js @@ -1,5 +1,8 @@ +const lexical = require('./lexical.js'); +const syntax = require('./syntax.js'); + module.exports = function(Filter, Tag) { - function render(tokens, ctx) { + function render(tokens, scope) { var html = ''; while (tokens.length) { var token = tokens.shift(); @@ -8,10 +11,10 @@ module.exports = function(Filter, Tag) { html += token.value; break; case 'output': - html += evaluate(token.value, ctx); + html += evalFilter(token.value, scope); break; case 'tag': - html += renderTag(token, tokens, ctx); + html += renderTag(token, tokens, scope); break; default: throw new Error(`unexpected type: ${token.type}`); @@ -20,20 +23,37 @@ module.exports = function(Filter, Tag) { return html; } - 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.construct(str)) - .reduce((v, filter) => filter.render(v, ctx), val); + 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); + } + } + return evalFilter(exp, scope); } - function renderTag(token, tokens, ctx) { + function evalFilter(str, scope){ + if(!scope) throw new Error('unable to evalFilter: scope undefined'); + var filters = str.split('|'); + var val = scope.get(filters.shift()); + return filters + .map(str => Filter.construct(str)) + .reduce((v, filter) => filter.render(v, scope), val); + } + + function renderTag(token, tokens, scope) { var tag = Tag.construct(token), subTokens = []; if (tag.needClose) { - var curToken, endToken = 'end' + tag.name; + var curToken, endToken = 'end' + tag.token.name; while ((curToken = tokens.shift()) && curToken.value !== endToken) { subTokens.push(curToken); } @@ -41,10 +61,10 @@ module.exports = function(Filter, Tag) { throw new Error(`${token.value} not closed`); } } - return tag.render(subTokens, ctx); + return tag.render(subTokens, scope); } return { - render, evaluate, renderTag + render, renderTag, evalFilter, evalExp }; }; diff --git a/context.js b/scope.js similarity index 65% rename from context.js rename to scope.js index f2d979447..0d2ce73ea 100644 --- a/context.js +++ b/scope.js @@ -1,7 +1,7 @@ const _ = require('lodash'); const lexical = require('./lexical.js'); -var context = { +var scope = { get: function(str) { str = str && str.trim(); if(!str) return ''; @@ -11,27 +11,27 @@ var context = { return lexical.parseLiteral(str); } if (lexical.isVariable(str)) { - for (var i = this.context.length - 1; i >= 0; i--) { - var v = _.get(this.context[i], str); + 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.context[this.context.length-1][k] = v; + this.scopes[this.scopes.length-1][k] = v; return this; }, push: function(ctx) { - return this.context.push(ctx); + return this.scopes.push(ctx); }, pop: function() { - return this.context.pop(); + return this.scopes.pop(); } }; exports.factory = function(_ctx) { - var ctx = Object.create(context); - ctx.context = [_ctx]; + var ctx = Object.create(scope); + ctx.scopes = [_ctx]; return ctx; }; diff --git a/syntax.js b/syntax.js new file mode 100644 index 000000000..a48ef9054 --- /dev/null +++ b/syntax.js @@ -0,0 +1,13 @@ +var operators = { + '==': (l, r) => l == r, + '!=': (l, r) => l != r, + '>': (l, r) => l > r, + '<': (l, r) => l < r, + '>=': (l, r) => l >= r, + '<=': (l, r) => l <= r, + 'contains': (l, r) => l.indexOf(r) > -1, + 'and': (l, r) => l && r, + 'or': (l, r) => l || r +}; + +exports.operators = operators; diff --git a/tag.js b/tag.js index 9edcba0b8..b27f2c29a 100644 --- a/tag.js +++ b/tag.js @@ -1,21 +1,21 @@ const lexical = require('./lexical.js'); var _tagInstance = { - render: function(tokens, ctx) { - var obj = hash(this.markup, ctx); - var res = this.tag.render(tokens, ctx, this.markup, obj); + render: function(tokens, scope) { + var obj = hash(this.token.args, scope); + var res = this.tag.render(tokens, scope, this.token, obj); return res === undefined ? '' : res; } }; -function hash(markup, ctx) { +function hash(markup, scope) { var obj = {}; - lexical.patterns.hash.lastIndex = 0; - while (match = lexical.patterns.hash.exec(markup)) { + lexical.hash.lastIndex = 0; + while (match = lexical.hash.exec(markup)) { var k = match[1], v = match[2]; if (!k) continue; - obj[k] = ctx.get(v); + obj[k] = scope.get(v); } return obj; } @@ -35,8 +35,7 @@ module.exports = function() { if (!tag) throw new Error(`tag ${token.name} not found`); var instance = Object.create(_tagInstance); - instance.name = token.name; - instance.markup = token.value; + instance.token = token; instance.tag = tag; instance.needClose = tag.needClose; return instance; diff --git a/tags/assign.js b/tags/assign.js index 77d2b7b44..1d485d1c3 100644 --- a/tags/assign.js +++ b/tags/assign.js @@ -1,14 +1,14 @@ var Liquid = require('..'); -var patterns = Liquid.lexical.patterns; -var re = new RegExp(`(${patterns.identifier.source})\\s*=(.*)`); +var lexical = Liquid.lexical; +var re = new RegExp(`(${lexical.identifier.source})\\s*=(.*)`); module.exports = function(liquid) { liquid.registerTag('assign', { - render: function(tokens, ctx, markup, hash) { - var match = markup.match(re); + render: function(tokens, scope, token, hash) { + var match = token.value.match(re); var k = match[1], v = match[2]; - ctx.set(k, liquid.evaluate(v, ctx)); + scope.set(k, liquid.evaluate(v, scope)); } }); diff --git a/tags/case.js b/tags/case.js index 0f8bdd1c8..09df829de 100644 --- a/tags/case.js +++ b/tags/case.js @@ -1,34 +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})`); +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, ctx, markup, hash) { - var match = markup.match(caseRE); - var cond = liquid.evaluate(match[1], ctx); + 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 token = tokens[i]; - if (token.type === 'tag' && token.name === 'when') { + var tk = tokens[i]; + if (tk.type === 'tag' && tk.name === 'when') { if (matching) break; - match = token.value.match(whenRE); + match = tk.value.match(whenRE); if(!match) continue; - var val = liquid.evaluate(match[1], ctx); + var val = liquid.evaluate(match[1], scope); if (val === cond) matching = true; - } else if (token.type === 'tag' && token.name === 'else') { + } else if (tk.type === 'tag' && tk.name === 'else') { if (matching) break; else matching = true; - } else if (matching) partialTokens.push(token); + } else if (matching) partialTokens.push(tk); } - return liquid.renderTokens(partialTokens, ctx); + return liquid.renderTokens(partialTokens, scope); } }); }; diff --git a/tags/if.js b/tags/if.js new file mode 100644 index 000000000..910960878 --- /dev/null +++ b/tags/if.js @@ -0,0 +1,24 @@ +var Liquid = require('..'); +var lexical = Liquid.lexical; + +module.exports = function(liquid) { + liquid.registerTag('if', { + needClose: true, + render: function(tokens, scope, token, hash) { + var partialTokens = [], + matching = liquid.evaluate(token.args, scope); + for (var i = 0; i < tokens.length; i++) { + var tk = tokens[i]; + if (tk.type === 'tag' && tk.name === 'elsif') { + if (matching) break; + matching = liquid.evaluate(tk.args, scope); + } else if (tk.type === 'tag' && tk.name === 'else') { + if (matching) break; + else matching = true; + } else if (matching) partialTokens.push(tk); + } + return liquid.renderTokens(partialTokens, scope); + } + }); +}; + diff --git a/tags/unless.js b/tags/unless.js new file mode 100644 index 000000000..7eb49c0ea --- /dev/null +++ b/tags/unless.js @@ -0,0 +1,21 @@ +var Liquid = require('..'); +var lexical = Liquid.lexical; + +module.exports = function(liquid) { + liquid.registerTag('unless', { + needClose: true, + render: function(tokens, scope, token, hash) { + var partialTokens = [], + matching = !liquid.evaluate(token.args, scope); + for (var i = 0; i < tokens.length; i++) { + var tk = tokens[i]; + if (tk.type === 'tag' && tk.name === 'else') { + if (matching) break; + else matching = true; + } else if (matching) partialTokens.push(tk); + } + return liquid.renderTokens(partialTokens, scope); + } + }); +}; + diff --git a/test/context.js b/test/context.js deleted file mode 100644 index bb2af1ce0..000000000 --- a/test/context.js +++ /dev/null @@ -1,51 +0,0 @@ -var chai = require("chai"); -var should = chai.should(); -var expect = chai.expect; - -var context = require('../context.js'); - -describe('context', function() { - var ctx; - beforeEach(function(){ - ctx = context.factory({ - foo: 'bar', - bar: ['a', {b: [1, 2]}] - }); - }); - - it('should parse literal', function() { - ctx.get('2.3').should.equal(2.3); - ctx.get('(2..4)').should.deep.equal([2,3]); - ctx.get('"foo"').should.equal("foo"); - }); - - it('should get property', function() { - ctx.get('foo').should.equal('bar'); - }); - - it('should set property', function() { - ctx.set('foo', 'FOO'); - ctx.get('foo').should.equal('FOO'); - }); - - it('should get desendent property', function() { - ctx.get('bar[0]').should.equal('a'); - ctx.get('bar[1].b').should.deep.equal([1, 2]); - ctx.get('bar[1].b[1]').should.equal(2); - }); - - it('should push context', function() { - ctx.push({foo: 'foo', foo1: 'foo1'}); - ctx.get('foo').should.equal('foo'); - ctx.get('foo1').should.equal('foo1'); - ctx.get('bar[1].b[1]').should.equal(2); - }); - - it('should pop context', function() { - ctx.push({foo: 'foo', foo1: 'foo1'}); - ctx.pop(); - expect(ctx.get('foo')).to.equal('bar'); - expect(ctx.get('foo1')).to.equal(''); - expect(ctx.get('bar[1].b[1]')).to.equal(2); - }); -}); diff --git a/test/filter.js b/test/filter.js index 1e6fb1839..b8fbaf296 100644 --- a/test/filter.js +++ b/test/filter.js @@ -6,13 +6,13 @@ const expect = chai.expect; chai.use(sinonChai); var filter = require('../filter.js')(); -var context = require('../context.js'); +var Scope = require('../scope.js'); describe('filter', function() { - var ctx; + var scope; beforeEach(function(){ filter.clear(); - ctx = context.factory(); + scope = Scope.factory(); }); it('should throw when not registered', function() { expect(function() { @@ -22,13 +22,13 @@ describe('filter', function() { it('should register a simple filter', function(){ filter.register('foo', x => x.toUpperCase()); - expect(filter.construct('foo').render('foo', ctx)).to.equal('FOO'); + expect(filter.construct('foo').render('foo', scope)).to.equal('FOO'); }); it('should call filter with corrct arguments', function(){ var spy = sinon.spy(); filter.register('foo', spy); - filter.construct('foo: 33').render('foo', ctx); + filter.construct('foo: 33').render('foo', scope); expect(spy).to.have.been.calledWith('foo', 33); }); }); diff --git a/test/filters.js b/test/filters.js index 0c60f09be..a4fb0231d 100644 --- a/test/filters.js +++ b/test/filters.js @@ -5,12 +5,49 @@ var liquid = require('..')(); describe('filters', function() { var ctx = { + date: new Date('1995-12-17T03:24:00'), foo: 'bar', arr: [-2, 'a'] }; it('should support abs', function() { - expect(liquid.render('{{ -3 | abs }}', ctx)).to.equal('3'); + expect(liquid.render('{{ -3 | abs }}')).to.equal('3'); expect(liquid.render('{{ arr[0] | abs }}', ctx)).to.equal('2'); }); + + it('should support append', function() { + expect(liquid.render('{{ -3 | append: "abc" }}')).to.equal('-3abc'); + expect(liquid.render('{{ "a" | append: foo }}', ctx)).to.equal('abar'); + }); + + it('should support capitalize', function() { + expect(liquid.render('{{ "i am good" | capitalize }}')).to.equal('I am good'); + }); + + it('should support ceil', function() { + expect(liquid.render('{{ 1.2 | ceil }}')).to.equal('2'); + expect(liquid.render('{{ 2.0 | ceil }}')).to.equal('2'); + expect(liquid.render('{{ "3.5" | ceil }}')).to.equal('4'); + expect(liquid.render('{{ 183.357 | ceil }}')).to.equal('184'); + }); + + it('should support date', function() { + expect(liquid.render('{{ date | date:"%Y-%m-%d %H:%M:%S"}}', ctx)).to.equal('1995-12-17 11:24:00'); + expect(liquid.render('{{ date | date:"%a, %b %d, %y"}}', ctx)).to.equal('Sun, Dec 17, 95'); + }); + + it('should support default', function() { + expect(liquid.render('{{false |default: "a"}}')).to.equal('a'); + }); + + it('should support divided_by', function() { + expect(liquid.render('{{4 | divided_by: 2}}')).to.equal('2'); + expect(liquid.render('{{16 | divided_by: 4}}')).to.equal('4'); + expect(liquid.render('{{5 | divided_by: 3}}')).to.equal('1'); + }); + + it('should support downcase', function() { + expect(liquid.render('{{ "Parker Moore" | downcase }}')).to.equal('parker moore'); + expect(liquid.render('{{ "apple" | downcase }}')).to.equal('apple'); + }); }); diff --git a/test/render.js b/test/render.js index eebd1850d..0015ec81d 100644 --- a/test/render.js +++ b/test/render.js @@ -6,15 +6,20 @@ const expect = chai.expect; chai.use(sinonChai); var tag = require('../tag.js')(); -var context = require('../context.js'); +var Scope = require('../scope.js'); var filter = require('../filter')(); -var render = require('../render.js')(filter, tag).render; +var Render = require('../render.js')(filter, tag); +var render = Render.render; +var evalExp = Render.evalExp; +var evalFilter = Render.evalFilter; describe('render', function() { - var ctx, htmlToken, tagToken, filterToken; + var scope, htmlToken, tagToken, filterToken; before(function() { - ctx = context.factory({ + scope = Scope.factory({ + one: 1, + two: 2, x: 'XXX', foo: { bar: ['a', 2] @@ -23,7 +28,8 @@ describe('render', function() { tagToken = { type: 'tag', value: 'foo bar:x foo:"FOO" num:2.3', - name: 'foo' + name: 'foo', + args: 'bar:x foo:"FOO" num:2.3' }; htmlToken = { type: 'html', @@ -41,21 +47,21 @@ describe('render', function() { }); it('should render html', function() { - expect(render([htmlToken], ctx)).to.equal('

'); + expect(render([htmlToken], scope)).to.equal('

'); }); it('should render with tag function', function() { tag.register('foo', { render: x => 'X' }); - expect(render([tagToken], ctx)).to.equal('X'); + expect(render([tagToken], scope)).to.equal('X'); }); it('should call tag with correct arguments', function() { var spy = sinon.spy(); tag.register('foo', { render: spy }); - render([tagToken], ctx); - expect(spy).to.have.been.calledWithMatch([], ctx, tagToken.value, { + render([tagToken], scope); + expect(spy).to.have.been.calledWithMatch([], scope, tagToken, { bar: 'XXX', foo: 'FOO', num: 2.3 @@ -65,7 +71,7 @@ describe('render', function() { it('should render with filter function', function() { filter.register('date', (l, r) => l + r); filter.register('time', (l, r) => l + 3*r); - expect(render([filterToken], ctx)).to.equal('ab6'); + expect(render([filterToken], scope)).to.equal('ab6'); }); it('should call filter with correct arguments', function() { @@ -73,8 +79,23 @@ describe('render', function() { var time = sinon.spy(); filter.register('date', date); filter.register('time', time); - render([filterToken], ctx); + render([filterToken], scope); expect(date).to.have.been.calledWith('a', 'b'); expect(time).to.have.been.calledWith('y', 2); }); + + 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 new file mode 100644 index 000000000..08021b00f --- /dev/null +++ b/test/scope.js @@ -0,0 +1,51 @@ +var chai = require("chai"); +var should = chai.should(); +var expect = chai.expect; + +var Scope = require('../scope.js'); + +describe('scope', function() { + var scope; + beforeEach(function(){ + scope = Scope.factory({ + foo: 'bar', + bar: ['a', {b: [1, 2]}] + }); + }); + + 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'); + }); + + it('should set property', function() { + scope.set('foo', 'FOO'); + scope.get('foo').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]); + scope.get('bar[1].b[1]').should.equal(2); + }); + + it('should push scope', function() { + scope.push({foo: 'foo', foo1: 'foo1'}); + scope.get('foo').should.equal('foo'); + scope.get('foo1').should.equal('foo1'); + scope.get('bar[1].b[1]').should.equal(2); + }); + + it('should pop 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('bar[1].b[1]')).to.equal(2); + }); +}); diff --git a/test/tag.js b/test/tag.js index c7e83189c..94d812ca3 100644 --- a/test/tag.js +++ b/test/tag.js @@ -5,12 +5,12 @@ var expect = chai.expect; chai.use(sinonChai); var tag = require('../tag.js')(); -var context = require('../context.js'); +var Scope = require('../scope.js'); describe('tag', function() { - var ctx; + var scope; before(function() { - ctx = context.factory({ + scope = Scope.factory({ foo: 'bar', arr: [2, 1] }); @@ -52,7 +52,7 @@ describe('tag', function() { type: 'tag', value: 'foo', name: 'foo' - }).render(tokens, ctx); + }).render(tokens, scope); expect(spy).to.have.been.called; }); @@ -62,13 +62,14 @@ describe('tag', function() { tag.register('foo', { render: spy }); - var t = tag.construct({ + var token = { 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', { + name: 'foo', + args: 'aa:foo bb: arr[0] cc: 2.3' + }; + tag.construct(token).render(tokens, scope); + expect(spy).to.have.been.calledWithMatch(tokens, scope, token, { aa: 'bar', bb: 2, cc: 2.3 diff --git a/test/tags.js b/test/tags.js index 8aeae51c6..b7c4aacd8 100644 --- a/test/tags.js +++ b/test/tags.js @@ -4,6 +4,9 @@ const expect = chai.expect; var liquid = require('..')(); var ctx = { + one: 1, + two: 2, + leq: '<=', empty: '', foo: 'bar', arr: [-2, 'a'] @@ -30,5 +33,16 @@ describe('tags', function() { '{% when "b" %}b{% when "c"%}c{%else %}d' + '{%endcase%}')).to.equal('d'); }); + + 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'); + }); }); diff --git a/tokenizer.js b/tokenizer.js index 350cbbe61..36157201c 100644 --- a/tokenizer.js +++ b/tokenizer.js @@ -16,19 +16,28 @@ function parse(html){ value: htmlFragment }); } - if(result[1]){ - var match = result[1].trim().match(lexical.patterns.identifier); - if (!match) throw new Error('illegal tag: ' + result[1]); + 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]; + tokens.push({ type: 'tag', - name: match[0], - value: result[2].trim() + value: cleanTag.trim(), + raw: rawTag, + args, name }); } else{ tokens.push({ type: 'output', - value: result[4].trim() + raw: rawOut, + value: cleanOut.trim() }); } idx = syntax.lastIndex;