This commit is contained in:
harttle
2016-06-15 22:37:24 +08:00
parent d5ecce9c8b
commit 58daab6128
23 changed files with 402 additions and 215 deletions
+65 -58
View File
@@ -11,74 +11,79 @@ shall be implemented.
> Shopify liquid is used by [Jekyll][jekyll] and [Github Pages][gh].
## Operators
Documentation: <https://shopify.github.io/liquid/basics/operators/>
`==`, `!=`, `>`, `<`, `>=`, `<=`, `or`, `and`, `contains`.
## Tags
Documentation: <https://shopify.github.io/liquid/basics/introduction/#tags>
- [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: <https://shopify.github.io/liquid/basics/introduction/#filters>
- [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
+3 -3
View File
@@ -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);
}
+13
View File
@@ -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());
};
-3
View File
@@ -1,3 +0,0 @@
module.exports = function(liquid) {
liquid.registerFilter('abs', v => Math.abs(v));
};
+8 -9
View File
@@ -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;
}
+21 -9
View File
@@ -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
};
+1 -1
View File
@@ -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": {
+34 -14
View File
@@ -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
};
};
+8 -8
View File
@@ -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;
};
+13
View File
@@ -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;
+8 -9
View File
@@ -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;
+5 -5
View File
@@ -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));
}
});
+13 -13
View File
@@ -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);
}
});
};
+24
View File
@@ -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);
}
});
};
+21
View File
@@ -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);
}
});
};
-51
View File
@@ -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);
});
});
+5 -5
View File
@@ -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);
});
});
+38 -1
View File
@@ -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');
});
});
+32 -11
View File
@@ -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('<p>');
expect(render([htmlToken], scope)).to.equal('<p>');
});
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);
});
});
+51
View File
@@ -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);
});
});
+10 -9
View File
@@ -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
+14
View File
@@ -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 one<two %}a{%endif%}', ctx)).to.equal('');
expect(liquid.render('{% if false %}yes{%else%}no{%endif%}', ctx)).to.equal('no');
});
it('should support unless', function(){
expect(liquid.render('{% unless 1 %}yes{%else%}no{%endunless%}', ctx)).to.equal('no');
expect(liquid.render('{% unless 1>2 %}yes{%endunless%}', ctx)).to.equal('yes');
});
});
+15 -6
View File
@@ -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;