fix existing tags

new tags: for,cycle
This commit is contained in:
harttle
2016-06-17 08:39:23 +08:00
parent 8b9e9f759a
commit 0b7da2abf7
24 changed files with 432 additions and 195 deletions
+9 -9
View File
@@ -25,18 +25,18 @@ Documentation: <https://shopify.github.io/liquid/basics/introduction/#tags>
- [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] 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] 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] - [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] - [x] 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] - [x] break [Document](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/for.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] - [x] continue [Document](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/for.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] - [x] for [Document: limit,offset,range,reversed)](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/for.js) [Test][tt]
- [ ] cycle [Document](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt] - [x] cycle [Document](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/cycle.js) [Test][tt]
- [ ] cycle [Document: group)](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt] - [x] cycle [Document: group)](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/cycle.js) [Test][tt]
- [ ] tablerow [Document](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt] - [ ] tablerow [Document](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] - [ ] tablerow [Document: cols,limit,offset,range](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt]
- [x] assign [Document](https://shopify.github.io/liquid/tags/variable/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/assign.js) [Test][tt] - [x] assign [Document](https://shopify.github.io/liquid/tags/variable/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/assign.js) [Test][tt]
- [x] capture [Document](https://shopify.github.io/liquid/tags/variable/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt] - [x] capture [Document](https://shopify.github.io/liquid/tags/variable/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/capture.js) [Test][tt]
- [x] increment [Document](https://shopify.github.io/liquid/tags/variable/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt] - [x] increment [Document](https://shopify.github.io/liquid/tags/variable/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/increment.js) [Test][tt]
- [x] decrement [Document](https://shopify.github.io/liquid/tags/variable/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt] - [x] decrement [Document](https://shopify.github.io/liquid/tags/variable/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/decrement.js) [Test][tt]
## Filters ## Filters
+54
View File
@@ -0,0 +1,54 @@
const syntax = require('./syntax.js');
const Exp = require('./expression.js');
const lexical = require('./lexical.js');
const _ = require('lodash');
function evalExp(exp, scope) {
if (!scope) throw new Error('unable to evalExp: scope undefined');
var operatorREs = lexical.operators;
for (var i = 0; i < operatorREs.length; i++) {
var operatorRE = operatorREs[i];
var expRE = new RegExp(`^(${lexical.quoteBalanced.source})(${operatorRE.source})(${lexical.quoteBalanced.source})$`);
var match = exp.match(expRE);
if (match) {
var l = evalExp(match[1], scope);
var op = syntax.operators[match[2].trim()];
var r = evalExp(match[3], scope);
return op(l, r);
}
}
if (match = exp.match(lexical.rangeLine)) {
var low = evalValue(match[1], scope),
high = evalValue(match[2], scope) + 1;
return _.range(low, high);
}
return evalValue(exp, scope);
}
function evalValue(str, scope) {
str = str && str.trim();
if (!str) return undefined;
if (lexical.isLiteral(str)) {
var a = lexical.parseLiteral(str);
return lexical.parseLiteral(str);
}
if (lexical.isVariable(str)) {
return scope.get(str);
}
}
function isTruthy(val) {
if (val instanceof Array) return !!val.length;
return !!val;
}
function isFalsy(val) {
return !isTruthy(val);
}
module.exports = {
evalExp, evalValue, isTruthy, isFalsy
};
+2 -1
View File
@@ -1,9 +1,10 @@
const lexical = require('./lexical.js'); const lexical = require('./lexical.js');
const _ = require('lodash'); const _ = require('lodash');
const Exp = require('./expression.js');
var _filterInstance = { var _filterInstance = {
render: function(output, scope) { render: function(output, scope) {
var args = this.args.map(arg => scope.get(arg)); var args = this.args.map(arg => Exp.evalValue(arg, scope));
args.unshift(output); args.unshift(output);
return this.filter.apply(null, args); return this.filter.apply(null, args);
} }
+7 -3
View File
@@ -8,6 +8,7 @@ const Tag = require('./tag.js');
const Filter = require('./filter.js'); const Filter = require('./filter.js');
const error = require('./error.js'); const error = require('./error.js');
const Template = require('./template'); const Template = require('./template');
const Expression = require('./expression.js');
const tagsPath = path.join(__dirname, "tags"); const tagsPath = path.join(__dirname, "tags");
@@ -33,14 +34,13 @@ function factory(){
engine.parseStream = template.parseStream; engine.parseStream = template.parseStream;
var renderer = Render(engine.filter, engine.tag); var renderer = Render(engine.filter, engine.tag);
engine.evaluate = renderer.evalExp;
engine.evalExp = renderer.evalExp;
engine.evalFilter = renderer.evalFilter; engine.evalFilter = renderer.evalFilter;
engine.renderTemplates = renderer.renderTemplates; engine.renderTemplates = renderer.renderTemplates;
engine.render = function(html, ctx) { engine.render = function(html, ctx) {
var tokens = engine.tokenize(html); var tokens = engine.tokenize(html);
var templates = engine.parse(tokens); var templates = engine.parse(tokens);
engine.register = {};
return engine.renderTemplates(templates, scope.factory(ctx)); return engine.renderTemplates(templates, scope.factory(ctx));
}, },
@@ -55,7 +55,11 @@ function factory(){
factory.lexical = lexical; factory.lexical = lexical;
factory.error = error; factory.error = error;
factory.isTruthy = Render.isTruthy; factory.isTruthy = Expression.isTruthy;
factory.isFalsy = Expression.isFalsy;
factory.stringify = Render.stringify; factory.stringify = Render.stringify;
factory.evalExp = Expression.evalExp;
factory.evalValue = Expression.evalValue;
module.exports = factory; module.exports = factory;
+18 -13
View File
@@ -5,15 +5,18 @@ var doubleQuoted = /"[^"]*"/;
var number = /-?\d+\.?\d*|\.?\d+/; var number = /-?\d+\.?\d*|\.?\d+/;
var bool = /true|false/i; var bool = /true|false/i;
var range = /\((\d+)\.\.(\d+)\)/;
var identifier = /[a-zA-Z_$][a-zA-Z_$0-9]*/; var identifier = /[a-zA-Z_$][a-zA-Z_$0-9]*/;
var subscript = /\[\d+\]/; var subscript = /\[\d+\]/;
var quoted = new RegExp(`${singleQuoted.source}|${doubleQuoted.source}`); var quoted = new RegExp(`${singleQuoted.source}|${doubleQuoted.source}`);
var literal = new RegExp(`${quoted.source}|${range.source}|${bool.source}|${number.source}`, 'i'); var literal = new RegExp(`${quoted.source}|${bool.source}|${number.source}`, 'i');
var variable = new RegExp(`${identifier.source}(?:\\.${identifier.source}|${subscript.source})*`); var variable = new RegExp(`${identifier.source}(?:\\.${identifier.source}|${subscript.source})*`);
var value = new RegExp(`${literal.source}|${variable.source}`, 'i'); var value = new RegExp(`${literal.source}|${variable.source}`, 'i');
var hash = new RegExp(`(${identifier.source})\\s*:\\s*(${value.source})`, 'g'); var range = new RegExp(`\\((?:${value.source})\\.\\.(?:${value.source})\\)`);
var rangeCapture = new RegExp(`\\((${value.source})\\.\\.(${value.source})\\)`);
var variableOrRange = new RegExp(`${variable.source}|${range.source}`);
var hash = new RegExp(`(?:${identifier.source})\\s*:\\s*(?:${value.source})`, 'g');
var hashCapture = new RegExp(`(${identifier.source})\\s*:\\s*(${value.source})`, 'g');
var filter = new RegExp(`(${identifier.source})(?:\\s*:\\s*(${value.source}))?`); var filter = new RegExp(`(${identifier.source})(?:\\s*:\\s*(${value.source}))?`);
var quoteBalanced = new RegExp(`(?:${singleQuoted.source}|${doubleQuoted.source}|[^'"])*`); var quoteBalanced = new RegExp(`(?:${singleQuoted.source}|${doubleQuoted.source}|[^'"])*`);
@@ -23,7 +26,7 @@ var variableLine = new RegExp(`^(?:${variable.source})$`);
var numberLine = new RegExp(`^(?:${number.source})$`); var numberLine = new RegExp(`^(?:${number.source})$`);
var boolLine = new RegExp(`^(?:${bool.source})$`, 'i'); var boolLine = new RegExp(`^(?:${bool.source})$`, 'i');
var quotedLine = new RegExp(`^(?:${quoted.source})$`); var quotedLine = new RegExp(`^(?:${quoted.source})$`);
var rangeLine = new RegExp(`^(?:${range.source})$`); var rangeLine = new RegExp(`^(?:${rangeCapture.source})$`);
var filterLine = new RegExp(`^(?:${filter.source})$`); var filterLine = new RegExp(`^(?:${filter.source})$`);
var operators = [ var operators = [
@@ -33,15 +36,19 @@ var operators = [
]; ];
function isLiteral (str) { function isLiteral(str) {
return literalLine.test(str); return literalLine.test(str);
} }
function isVariable (str) { function isRange(str) {
return rangeLine.test(str);
}
function isVariable(str) {
return variableLine.test(str); return variableLine.test(str);
} }
function parseLiteral (str) { function parseLiteral(str) {
var res; var res;
if (res = str.match(numberLine)) { if (res = str.match(numberLine)) {
return Number(str); return Number(str);
@@ -52,15 +59,13 @@ function parseLiteral (str) {
if (res = str.match(quotedLine)) { if (res = str.match(quotedLine)) {
return str.slice(1, -1); return str.slice(1, -1);
} }
if (res = str.match(rangeLine)) {
return _.range(res[1], res[2]);
}
} }
module.exports = { module.exports = {
quoted, number, bool, range, literal, hash, filter, quoted, number, bool, literal, filter,
hash, hashCapture,
range, rangeCapture, variableOrRange,
identifier, value, quoteBalanced, operators, identifier, value, quoteBalanced, operators,
quotedLine, numberLine, boolLine, rangeLine, literalLine, filterLine, tagLine, quotedLine, numberLine, boolLine, rangeLine, literalLine, filterLine, tagLine,
isLiteral, isVariable, parseLiteral isLiteral, isVariable, parseLiteral, isRange
}; };
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "shopify-liquid", "name": "shopify-liquid",
"version": "1.0.2", "version": "1.0.3",
"description": "A Shopify Liquid Implementation in Node.js", "description": "A Shopify Liquid Implementation in Node.js",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
+17 -31
View File
@@ -1,66 +1,52 @@
const lexical = require('./lexical.js'); const lexical = require('./lexical.js');
const syntax = require('./syntax.js');
const error = require('./error.js'); const error = require('./error.js');
const Exp = require('./expression.js');
function stringify(val) { function stringify(val) {
if (typeof val === 'string') return val; if (typeof val === 'string') return val;
return JSON.stringify(val); return JSON.stringify(val);
} }
function isTruthy(val) {
if (val instanceof Array) return !!val.length;
return !!val;
}
function factory(Filter, Tag) { function factory(Filter, Tag) {
function renderTemplates(templates, scope) { function renderTemplates(templates, scope) {
var html = ''; var html = '';
var template; templates.some(template => {
while (template = templates.shift()) { if (scope.get('forloop.skip')) return true;
if (template.type === 'tag') { if (template.type === 'tag') {
html += template.render(scope); if (template.name === 'continue') {
scope.set('forloop.skip', true);
return true;
}
if (template.name === 'break') {
scope.set('forloop.stop', true);
scope.set('forloop.skip', true);
return true;
}
html += template.render(scope, this.register);
} else if (template.type === 'html') { } else if (template.type === 'html') {
html += template.value; html += template.value;
} else if (template.type === 'output') { } else if (template.type === 'output') {
var val = evalFilter(template.value, scope); var val = evalFilter(template.value, scope);
html += stringify(val); html += stringify(val);
} }
} });
return html; return html;
} }
function evalExp(exp, scope) {
if (!scope) error('unable to evalExp: scope undefined');
var operatorREs = lexical.operators;
for (var i = 0; i < operatorREs.length; i++) {
var operatorRE = operatorREs[i];
var expRE = new RegExp(`^(${lexical.quoteBalanced.source})(${operatorRE.source})(${lexical.quoteBalanced.source})$`);
var match = exp.match(expRE);
if (match) {
var l = evalExp(match[1], scope);
var op = syntax.operators[match[2].trim()];
var r = evalExp(match[3], scope);
return op(l, r);
}
}
return evalFilter(exp, scope);
}
function evalFilter(str, scope) { function evalFilter(str, scope) {
if (!scope) error('unable to evalFilter: scope undefined'); if (!scope) throw new Error('unable to evalFilter: scope undefined');
var filters = str.split('|'); var filters = str.split('|');
var val = scope.get(filters.shift()); var val = Exp.evalValue(filters.shift(), scope);
return filters return filters
.map(str => Filter.construct(str)) .map(str => Filter.construct(str))
.reduce((v, filter) => filter.render(v, scope), val); .reduce((v, filter) => filter.render(v, scope), val);
} }
return { return {
renderTemplates, evalFilter, evalExp renderTemplates, evalFilter
}; };
}; };
factory.isTruthy = isTruthy;
factory.stringify = stringify; factory.stringify = stringify;
module.exports = factory; module.exports = factory;
+6 -15
View File
@@ -4,27 +4,18 @@ const error = require('./error.js');
var scope = { var scope = {
get: function(str) { get: function(str) {
str = str && str.trim(); for (var i = this.scopes.length - 1; i >= 0; i--) {
if(!str) return ''; var v = _.get(this.scopes[i], str);
if (v !== undefined) return v;
if (lexical.isLiteral(str)) {
var a = lexical.parseLiteral(str);
return lexical.parseLiteral(str);
}
if (lexical.isVariable(str)) {
for (var i = this.scopes.length - 1; i >= 0; i--) {
var v = _.get(this.scopes[i], str);
if (v !== undefined) return v;
}
} }
return ''; return '';
}, },
set: function(k, v){ set: function(k, v) {
this.scopes[this.scopes.length-1][k] = v; _.set(this.scopes[this.scopes.length - 1], k, v);
return this; return this;
}, },
push: function(ctx) { push: function(ctx) {
if(!ctx) error(`trying to push ${ctx} into scopes`); if (!ctx) error(`trying to push ${ctx} into scopes`);
return this.scopes.push(ctx); return this.scopes.push(ctx);
}, },
pop: function() { pop: function() {
+11 -7
View File
@@ -1,24 +1,28 @@
const lexical = require('./lexical.js'); const lexical = require('./lexical.js');
const Exp = require('./expression.js');
var _tagInstance = { var _tagInstance = {
render: function(scope) { render: function(scope, register) {
var reg = register[this.name];
if(!reg) reg = register[this.name] = {}
var obj = hash(this.token.args, scope); var obj = hash(this.token.args, scope);
return this.tagImpl.render(scope, obj) || ''; return this.tagImpl.render(scope, obj, reg) || '';
}, },
parse: function(tokens){ parse: function(tokens){
this.tagImpl.parse(this.token, tokens); if(this.tagImpl.parse){
this.tagImpl.parse(this.token, tokens);
}
return this; return this;
} }
}; };
function hash(markup, scope) { function hash(markup, scope) {
var obj = {}; var obj = {};
lexical.hash.lastIndex = 0; lexical.hashCapture.lastIndex = 0;
while (match = lexical.hash.exec(markup)) { while (match = lexical.hashCapture.exec(markup)) {
var k = match[1], var k = match[1],
v = match[2]; v = match[2];
if (!k) continue; obj[k] = Exp.evalValue(v, scope);
obj[k] = scope.get(v);
} }
return obj; return obj;
} }
+7 -3
View File
@@ -5,10 +5,14 @@ var re = new RegExp(`(${lexical.identifier.source})\\s*=(.*)`);
module.exports = function(liquid) { module.exports = function(liquid) {
liquid.registerTag('assign', { liquid.registerTag('assign', {
render: function(tokens, scope, token, hash) { parse: function(token){
var match = token.args.match(re); var match = token.args.match(re);
var k = match[1], v = match[2]; if(!match) error(`illegal token ${token.raw}`, token);
scope.set(k, liquid.evaluate(v, scope)); this.key = match[1];
this.value = match[2];
},
render: function(scope, hash) {
scope.set(this.key, Liquid.evalValue(this.value, scope));
} }
}); });
+34 -23
View File
@@ -1,34 +1,45 @@
var Liquid = require('..'); var Liquid = require('..');
var lexical = Liquid.lexical; 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) { module.exports = function(liquid) {
liquid.registerTag('case', { liquid.registerTag('case', {
needClose: true,
render: function(tokens, scope, token, hash) {
var match = token.value.match(caseRE);
var cond = liquid.evaluate(match[1], scope);
var partialTokens = [], parse: function(tagToken, remainTokens) {
matching = false; this.cond = tagToken.args;
for (var i = 0; i < tokens.length; i++) { this.cases = [];
var tk = tokens[i]; this.elseTemplates = [];
if (tk.type === 'tag' && tk.name === 'when') {
if (matching) break;
match = tk.value.match(whenRE);
if(!match) continue;
var val = liquid.evaluate(match[1], scope); var p = [],
if (val === cond) matching = true; stream = liquid.parseStream(remainTokens)
} else if (tk.type === 'tag' && tk.name === 'else') { .onTag('when', token => {
if (matching) break; if (!this.cases[token.args]) {
else matching = true; this.cases.push({
} else if (matching) partialTokens.push(tk); val: token.args,
templates: p = []
});
}
})
.onTag('else', token => p = this.elseTemplates)
.onTag('endcase', token => stream.stop())
.onTemplate(tpl => p.push(tpl))
.onEnd(x => {
throw new Error(`tag ${tagToken.raw} not closed`);
});
stream.start();
},
render: function(scope, hash) {
for (var i = 0; i < this.cases.length; i++) {
var branch = this.cases[i];
var val = Liquid.evalExp(branch.val, scope);
var cond = Liquid.evalExp(this.cond, scope);
if (val === cond) {
return liquid.renderTemplates(branch.templates, scope);
}
} }
return liquid.renderTemplates(this.elseTemplates, scope);
return liquid.renderTokens(partialTokens, scope);
} }
}); });
}; };
+43
View File
@@ -0,0 +1,43 @@
var Liquid = require('..');
var lexical = Liquid.lexical;
var groupRE = new RegExp(`^(?:(${lexical.value.source})\\s*:\\s*)?(.*)$`);
var candidatesRE = new RegExp(lexical.value.source, 'g');
module.exports = function(liquid) {
liquid.registerTag('cycle', {
parse: function(tagToken, remainTokens) {
var match = groupRE.exec(tagToken.args);
if(!match) throw new Error(`illegal tag: ${tagToken.raw}`);
this.group = match[1] || '';
var candidates = match[2];
this.candidates = [];
while(match = candidatesRE.exec(candidates)){
this.candidates.push(match[0]);
}
if (!this.candidates.length){
throw new Error(`illegal tag: ${tagToken.raw}`);
}
},
render: function(scope, hash, register) {
var fingerprint = Liquid.evalValue(this.group, scope) + ':' +
this.candidates.join(',');
var idx = register[fingerprint];
if(idx === undefined){
idx = register[fingerprint] = 0;
}
var candidate = this.candidates[idx];
idx = (idx + 1) % this.candidates.length;
register[fingerprint] = idx;
return Liquid.evalValue(candidate, scope);
}
});
};
+8 -5
View File
@@ -5,12 +5,15 @@ const error = Liquid.error;
module.exports = function(liquid) { module.exports = function(liquid) {
liquid.registerTag('decrement', { liquid.registerTag('decrement', {
render: function(tokens, scope, token, hash) { parse: function(token) {
var match = token.args.match(lexical.identifier); var match = token.args.match(lexical.identifier);
if(!match) error(`illegal identifier ${token.args}`, token); if (!match) error(`illegal identifier ${token.args}`, token);
var k = match[0], v = scope.get(k); this.variable = match[0];
if(typeof v !== 'number') v = 0; },
scope.set(k, v-1); render: function(scope, hash) {
var v = scope.get(this.variable);
if (typeof v !== 'number') v = 0;
scope.set(this.variable, v - 1);
} }
}); });
+37 -5
View File
@@ -1,15 +1,19 @@
var Liquid = require('..'); var Liquid = require('..');
var lexical = Liquid.lexical; var lexical = Liquid.lexical;
var re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+(.+)$`); var re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+` +
`(${lexical.variableOrRange.source})` +
`(?:\\s+${lexical.hash.source})*` +
`(?:\\s+(reversed))?$`);
module.exports = function(liquid) { module.exports = function(liquid) {
liquid.registerTag('for', { liquid.registerTag('for', {
parse: function(tagToken, remainTokens) { parse: function(tagToken, remainTokens) {
var match = re.exec(tagToken.args); var match = re.exec(tagToken.args);
if(!match) throw new Error(`illegal tag: ${tagToken.raw}`); if (!match) throw new Error(`illegal tag: ${tagToken.raw}`);
this.variable = match[1]; this.variable = match[1];
this.collection = match[2]; this.collection = match[2];
this.reversed = !!match[3];
this.templates = []; this.templates = [];
this.elseTemplates = []; this.elseTemplates = [];
@@ -27,12 +31,40 @@ module.exports = function(liquid) {
}, },
render: function(scope, hash) { render: function(scope, hash) {
var collection = liquid.evaluate(this.collection, scope); var collection = Liquid.evalExp(this.collection, scope);
if(!(collection instanceof Array) || !collection.length){ if (Liquid.isFalsy(collection)) {
return liquid.renderTemplates(this.elseTemplates, scope); return liquid.renderTemplates(this.elseTemplates, scope);
} }
liquid.renderTemplates(this.templates, scope); var html = '',
ctx = {},
length = collection.length;
var offset = hash.offset || 0;
var limit = (hash.limit === undefined) ? collection.length : hash.limit;
var collection = collection.slice(offset, offset + limit);
if(this.reversed) collection.reverse();
collection.some((item, i) => {
ctx[this.variable] = item;
ctx.forloop = {
first: i === 0,
index: i + 1,
index0: i,
last: i === length - 1,
length: length,
rindex: length - i,
rindex0: length - i - 1,
stop: false,
skip: false
};
scope.push(ctx);
html += liquid.renderTemplates(this.templates, scope);
var breakloop = scope.get('forloop.stop');
scope.pop(ctx);
if (breakloop) return true;
});
return html;
} }
}); });
}; };
+2 -1
View File
@@ -5,6 +5,7 @@ module.exports = function(liquid) {
liquid.registerTag('if', { liquid.registerTag('if', {
parse: function(tagToken, remainTokens) { parse: function(tagToken, remainTokens) {
this.branches = []; this.branches = [];
this.elseTemplates = []; this.elseTemplates = [];
@@ -34,7 +35,7 @@ module.exports = function(liquid) {
render: function(scope, hash) { render: function(scope, hash) {
for (var i = 0; i < this.branches.length; i++) { for (var i = 0; i < this.branches.length; i++) {
var branch = this.branches[i]; var branch = this.branches[i];
var cond = liquid.evaluate(branch.cond, scope); var cond = Liquid.evalExp(branch.cond, scope);
if (Liquid.isTruthy(cond)) { if (Liquid.isTruthy(cond)) {
return liquid.renderTemplates(branch.templates, scope); return liquid.renderTemplates(branch.templates, scope);
} }
+8 -5
View File
@@ -5,12 +5,15 @@ const error = Liquid.error;
module.exports = function(liquid) { module.exports = function(liquid) {
liquid.registerTag('increment', { liquid.registerTag('increment', {
render: function(tokens, scope, token, hash) { parse: function(token) {
var match = token.args.match(lexical.identifier); var match = token.args.match(lexical.identifier);
if(!match) error(`illegal identifier ${token.args}`, token); if (!match) error(`illegal identifier ${token.args}`, token);
var k = match[0], v = scope.get(k); this.variable = match[0];
if(typeof v !== 'number') v = 0; },
scope.set(k, v+1); render: function(scope, hash) {
var v = scope.get(this.variable);
if (typeof v !== 'number') v = 0;
scope.set(this.variable, v + 1);
} }
}); });
+1 -1
View File
@@ -34,7 +34,7 @@ module.exports = function(liquid) {
render: function(scope, hash) { render: function(scope, hash) {
for (var i = 0; i < this.branches.length; i++) { for (var i = 0; i < this.branches.length; i++) {
var branch = this.branches[i]; var branch = this.branches[i];
var cond = liquid.evaluate(branch.cond, scope); var cond = Liquid.evalExp(branch.cond, scope);
cond = Liquid.isTruthy(cond); cond = Liquid.isTruthy(cond);
if (i === 0) cond = !cond; if (i === 0) cond = !cond;
if (cond) { if (cond) {
+6 -1
View File
@@ -26,7 +26,12 @@ module.exports = function(Tag) {
var template; var template;
if (token.type == 'tag') { if (token.type == 'tag') {
if (this.trigger(`tag:${token.name}`, token)) continue; if (this.trigger(`tag:${token.name}`, token)) continue;
template = parseTag(token, this.tokens); if (token.name === 'continue' || token.name === 'break'){
template = token;
}
else{
template = parseTag(token, this.tokens);
}
} else { } else {
template = token; template = token;
} }
+60
View File
@@ -0,0 +1,60 @@
const chai = require("chai");
const sinonChai = require("sinon-chai");
const sinon = require("sinon");
const expect = chai.expect;
chai.use(sinonChai);
var expression = require('../expression.js');
var Scope = require('../scope.js');
var evalExp = expression.evalExp;
var evalValue = expression.evalValue;
describe('expression', function() {
var scope;
beforeEach(function() {
scope = Scope.factory({
one: 1,
two: 2,
x: 'XXX'
});
});
it('should eval literals', function() {
expect(evalValue('2.3')).to.equal(2.3);
expect(evalValue('"foo"')).to.equal("foo");
});
it('should throw on illegal expression', function() {
expect(function() {
evalExp('1 contains "x"', scope);
}).to.throw();
});
it('should eval variables', function() {
expect(evalValue('23', scope)).to.equal(23);
expect(evalValue('one', scope)).to.equal(1);
expect(evalValue('x', scope)).to.equal('XXX');
});
it('should eval simple expression', function() {
expect(evalExp('1<2', scope)).to.equal(true);
expect(evalExp('2<=2', scope)).to.equal(true);
expect(evalExp('one<=two', scope)).to.equal(true);
expect(evalExp('x contains "x"', scope)).to.equal(false);
expect(evalExp('x contains "X"', scope)).to.equal(true);
expect(evalExp('x contains z', scope)).to.equal(true);
expect(evalExp('"<=" == "<="', scope)).to.equal(true);
});
it('should eval complex expression', function() {
expect(evalExp('1<2 and x contains "x"', scope)).to.equal(false);
expect(evalExp('1<2 or x contains "x"', scope)).to.equal(true);
});
it("should eval range expression", function() {
expect(evalExp('(2..4)', scope)).to.deep.equal([2,3,4]);
expect(evalExp('(two..4)', scope)).to.deep.equal([2,3,4]);
});
});
+6 -11
View File
@@ -17,6 +17,12 @@ describe('identifier', function() {
identifier.isLiteral('23').should.equal(true); identifier.isLiteral('23').should.equal(true);
}); });
it("should test range literal", function() {
identifier.isRange("(12..32)").should.equal(true);
identifier.isRange("(12..foo)").should.equal(true);
identifier.isRange("(foo.bar..foo)").should.equal(true);
});
it('should test string literal', function() { it('should test string literal', function() {
identifier.isLiteral('""').should.equal(true); identifier.isLiteral('""').should.equal(true);
identifier.isLiteral('"a\'b"').should.equal(true); identifier.isLiteral('"a\'b"').should.equal(true);
@@ -24,10 +30,6 @@ describe('identifier', function() {
identifier.isLiteral("'a bcd'").should.equal(true); identifier.isLiteral("'a bcd'").should.equal(true);
}); });
it("should test range literal", function() {
identifier.isLiteral("(12..32)").should.equal(true);
});
it("should test variable", function() { it("should test variable", function() {
identifier.isVariable("foo").should.equal(true); identifier.isVariable("foo").should.equal(true);
identifier.isVariable("foo.bar.foo").should.equal(true); identifier.isVariable("foo.bar.foo").should.equal(true);
@@ -65,11 +67,4 @@ describe('identifier', function() {
identifier.parseLiteral('"ab\'c"').should.equal("ab\'c"); identifier.parseLiteral('"ab\'c"').should.equal("ab\'c");
}); });
it("should parse range literal", function() {
var arr = identifier.parseLiteral("(12..32)");
arr.length.should.equal(20);
arr[0].should.equal(12);
arr[1].should.equal(13);
arr[19].should.equal(31);
});
}); });
-19
View File
@@ -10,7 +10,6 @@ var Scope = require('../scope.js');
var filter = require('../filter')(); var filter = require('../filter')();
var Render = require('../render.js')(filter, tag); var Render = require('../render.js')(filter, tag);
var render = Render.renderTemplates; var render = Render.renderTemplates;
var evalExp = Render.evalExp;
var evalFilter = Render.evalFilter; var evalFilter = Render.evalFilter;
describe('render', function() { describe('render', function() {
@@ -18,9 +17,6 @@ describe('render', function() {
beforeEach(function() { beforeEach(function() {
scope = Scope.factory({ scope = Scope.factory({
one: 1,
two: 2,
x: 'XXX',
foo: { foo: {
bar: ['a', 2] bar: ['a', 2]
} }
@@ -51,19 +47,4 @@ describe('render', function() {
filter.register('time', (l, r) => l + 3 * r); filter.register('time', (l, r) => l + 3 * r);
expect(evalFilter('foo.bar[0] | date: "b" | time:2', scope)).to.equal('ab6'); expect(evalFilter('foo.bar[0] | date: "b" | time:2', scope)).to.equal('ab6');
}); });
it('should eval expression', function() {
expect(evalExp('1<2', scope)).to.equal(true);
expect(evalExp('2<=2', scope)).to.equal(true);
expect(evalExp('one<=two', scope)).to.equal(true);
expect(function() {
evalExp('1 contains "x"', scope);
}).to.throw();
expect(evalExp('x contains "x"', scope)).to.equal(false);
expect(evalExp('x contains "X"', scope)).to.equal(true);
expect(evalExp('x contains z', scope)).to.equal(true);
expect(evalExp('1<2 and x contains "x"', scope)).to.equal(false);
expect(evalExp('1<2 or x contains "x"', scope)).to.equal(true);
expect(evalExp('"<=" == "<="', scope)).to.equal(true);
});
}); });
+5 -6
View File
@@ -13,12 +13,6 @@ describe('scope', function() {
}); });
}); });
it('should parse literal', function() {
scope.get('2.3').should.equal(2.3);
scope.get('(2..4)').should.deep.equal([2,3]);
scope.get('"foo"').should.equal("foo");
});
it('should get property', function() { it('should get property', function() {
scope.get('foo').should.equal('bar'); scope.get('foo').should.equal('bar');
}); });
@@ -28,6 +22,11 @@ describe('scope', function() {
scope.get('foo').should.equal('FOO'); scope.get('foo').should.equal('FOO');
}); });
it('should set child property', function() {
scope.set('oo.bar', 'FOO');
scope.get('oo.bar').should.equal('FOO');
});
it('should get desendent property', function() { it('should get desendent property', function() {
scope.get('bar[0]').should.equal('a'); scope.get('bar[0]').should.equal('a');
scope.get('bar[1].b').should.deep.equal([1, 2]); scope.get('bar[1].b').should.deep.equal([1, 2]);
+2 -2
View File
@@ -52,7 +52,7 @@ describe('tag', function() {
type: 'tag', type: 'tag',
value: 'foo', value: 'foo',
name: 'foo' name: 'foo'
}).render(scope); }).render(scope, {});
expect(spy).to.have.been.called; expect(spy).to.have.been.called;
}); });
@@ -68,7 +68,7 @@ describe('tag', function() {
name: 'foo', name: 'foo',
args: 'aa:foo bb: arr[0] cc: 2.3' args: 'aa:foo bb: arr[0] cc: 2.3'
}; };
tag.construct(token).render(scope); tag.construct(token).render(scope, {});
expect(spy).to.have.been.calledWithMatch(scope, { expect(spy).to.have.been.calledWithMatch(scope, {
aa: 'bar', aa: 'bar',
bb: 2, bb: 2,
+88 -33
View File
@@ -2,7 +2,7 @@ const chai = require("chai");
const expect = chai.expect; const expect = chai.expect;
var liquid = require('..')(), var liquid = require('..')(),
ctx; ctx, src, dst;
function test(src, dst) { function test(src, dst) {
expect(liquid.render(src, ctx)).to.equal(dst); expect(liquid.render(src, ctx)).to.equal(dst);
@@ -22,30 +22,29 @@ describe('tags', function() {
leq: '<=', leq: '<=',
empty: '', empty: '',
foo: 'bar', foo: 'bar',
arr: [-2, 'a', { arr: [-2, 'a'],
foo: 'bar' alpha: ['a', 'b', 'c'],
}],
emptyArray: [] emptyArray: []
}; };
}); });
//it('should support assign', function() { it('should support assign', function() {
//test('{% assign foo="bar"%}{{foo}}', 'bar'); test('{% assign foo="bar"%}{{foo}}', 'bar');
//}); });
//it('should support case', function() { it('should support case', function() {
//testThrow('{% case "foo"%}', /case "foo" not closed/); testThrow('{% case "foo"%}', /{% case "foo"%} not closed/);
//test('{% case "foo"%}' + test('{% case "foo"%}' +
//'{% when "foo" %}foo{% when "bar"%}bar' + '{% when "foo" %}foo{% when "bar"%}bar' +
//'{%endcase%}', 'foo'); '{%endcase%}', 'foo');
//test('{% case empty %}' + test('{% case empty %}' +
//'{% when "foo" %}foo{% when ""%}bar' + '{% when "foo" %}foo{% when ""%}bar' +
//'{%endcase%}', 'bar'); '{%endcase%}', 'bar');
//test('{% case false %}' + test('{% case false %}' +
//'{% when "foo" %}foo{% when ""%}bar' + '{% when "foo" %}foo{% when ""%}bar' +
//'{%endcase%}', ''); '{%endcase%}', '');
//test('{% case "a" %}' + test('{% case "a" %}' +
//'{% when "b" %}b{% when "c"%}c{%else %}d' + '{% when "b" %}b{% when "c"%}c{%else %}d' +
//'{%endcase%}', 'd'); '{%endcase%}', 'd');
//}); });
it('should support if', function() { it('should support if', function() {
testThrow('{% if false%}yes', /tag {% if false%} not closed/); testThrow('{% if false%}yes', /tag {% if false%} not closed/);
@@ -68,17 +67,73 @@ describe('tags', function() {
testThrow('{% capture = %}{%endcapture%}', /= not valid identifier/); testThrow('{% capture = %}{%endcapture%}', /= not valid identifier/);
}); });
//it('should support for', function() { it('should support for', function() {
//test('{%for i in arr%}{{"a" | capitalize}}{%endcapture%}{{f}}', 'A'); test('{%for c in alpha%}{{c}}{%endfor%}', 'abc');
//}); });
//it('should support increment', function() { it('should support for with forloop', function() {
//test('{% increment foo %}{%increment foo%}{{foo}}', '2'); src = '{%for c in alpha%}' +
//test('{% increment one %}{{one}}', '2'); '{{forloop.first}}.{{forloop.index}}.{{forloop.index0}}.' +
//}); '{{forloop.last}}.{{forloop.length}}.' +
'{{forloop.rindex}}.{{forloop.rindex0}}' +
'{{c}}\n' +
'{%endfor%}';
dst = 'true.1.0.false.3.3.2a\n' +
'false.2.1.false.3.2.1b\n' +
'false.3.2.true.3.1.0c\n';
test(src, dst);
});
//it('should support decrement', function() { it('should support for with continue and break', function() {
//test('{% decrement foo %}{%decrement foo%}{{foo}}', '-2'); src = '{% for i in (1..5) %}' +
//test('{% decrement one %}{{one}}', '0'); '{% if i == 4 %}{% continue %}' +
//}); '{% else %}{{ i }}' +
'{% endif %}' +
'{% endfor %}';
test(src, '1235');
src = '{% for i in (one..5) %}' +
'{% if i == 4 %}{% break %}{% endif %}' +
'{{ i }}' +
'{% endfor %}';
test(src, '123');
});
it('should support for with limit and offset', function() {
src = '{% for i in (1..5) limit:2 %}{{ i }}{% endfor %}';
test(src, '12');
src = '{% for i in (1..10) limit:2 offset:5%}{{ i }}{% endfor %}';
test(src, '67');
});
it('should support for reversed', function() {
src = '{% for i in (1..5) limit:2 reversed %}{{ i }}{% endfor %}';
test(src, '21');
});
it('should support cycle', function() {
src = "{% cycle '1', '2', '3' %}";
test(src + src + src + src, '1231');
});
it('should support cycle in for block', function() {
src = '{% for i in (1..5) %}{% cycle one, "e"%}{% endfor %}';
test(src, '1e1e1');
});
it('should support cycle group', function() {
src = "{% cycle one: '1', '2', '3'%}" +
"{% cycle 1: '1', '2', '3'%}" +
"{% cycle 2: '1', '2', '3'%}";
test(src, '121');
});
it('should support increment', function() {
test('{% increment foo %}{%increment foo%}{{foo}}', '2');
test('{% increment one %}{{one}}', '2');
});
it('should support decrement', function() {
test('{% decrement foo %}{%decrement foo%}{{foo}}', '-2');
test('{% decrement one %}{{one}}', '0');
});
}); });