-- [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;