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]. > Shopify liquid is used by [Jekyll][jekyll] and [Github Pages][gh].
## Operators
Documentation: <https://shopify.github.io/liquid/basics/operators/>
`==`, `!=`, `>`, `<`, `>=`, `<=`, `or`, `and`, `contains`.
## Tags ## Tags
Documentation: <https://shopify.github.io/liquid/basics/introduction/#tags> Documentation: <https://shopify.github.io/liquid/basics/introduction/#tags>
- [x] [assign](https://shopify.github.io/liquid/tags/control-flow/) - [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] [case/when](https://shopify.github.io/liquid/tags/control-flow/) - [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]
- [ ] [if](https://shopify.github.io/liquid/tags/control-flow/) - [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]
- [ ] [unless](https://shopify.github.io/liquid/tags/control-flow/) - [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]
- [ ] [elsif/else](https://shopify.github.io/liquid/tags/control-flow/) - [ ] for [Document](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/for.js) [Test][tt]
- [ ] [for](https://shopify.github.io/liquid/tags/iteration/) - [ ] break [Document](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt]
- [ ] [break](https://shopify.github.io/liquid/tags/iteration/) - [ ] continue [Document](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt]
- [ ] [continue](https://shopify.github.io/liquid/tags/iteration/) - [ ] 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]
- [ ] [for: limit,offset,range,reversed)](https://shopify.github.io/liquid/tags/iteration/) - [ ] cycle [Document](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt]
- [ ] [cycle](https://shopify.github.io/liquid/tags/iteration/) - [ ] cycle [Document: group)](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt]
- [ ] [cycle: group)](https://shopify.github.io/liquid/tags/iteration/) - [ ] tablerow [Document](https://shopify.github.io/liquid/tags/iteration/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt]
- [ ] [tablerow](https://shopify.github.io/liquid/tags/iteration/) - [ ] 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: cols,limit,offset,range](https://shopify.github.io/liquid/tags/iteration/) - [x] assign [Document](https://shopify.github.io/liquid/tags/variable/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/assign.js) [Test][tt]
- [ ] [assign](https://shopify.github.io/liquid/tags/variable/) - [ ] capture [Document](https://shopify.github.io/liquid/tags/variable/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt]
- [ ] [capture](https://shopify.github.io/liquid/tags/variable/) - [ ] increment [Document](https://shopify.github.io/liquid/tags/variable/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt]
- [ ] [increment](https://shopify.github.io/liquid/tags/variable/) - [ ] decrement [Document](https://shopify.github.io/liquid/tags/variable/) [Source](https://github.com/harttle/shopify-liquid/blob/master/tags/.js) [Test][tt]
- [ ] [decrement](https://shopify.github.io/liquid/tags/variable/)
## Filters ## Filters
Documentation: <https://shopify.github.io/liquid/basics/introduction/#filters> Documentation: <https://shopify.github.io/liquid/basics/introduction/#filters>
- [x] [abs](https://shopify.github.io/liquid/filters/abs/) - [x] abs [Document](https://shopify.github.io/liquid/filters/abs/) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [append](https://shopify.github.io/liquid/filters/append) - [x] append [Document](https://shopify.github.io/liquid/filters/append) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [capitalize](https://shopify.github.io/liquid/filters/capitalize) - [x] capitalize [Document](https://shopify.github.io/liquid/filters/capitalize) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [ceil](https://shopify.github.io/liquid/filters/ceil) - [x] ceil [Document](https://shopify.github.io/liquid/filters/ceil) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [date](https://shopify.github.io/liquid/filters/date) - [x] date [Document](https://shopify.github.io/liquid/filters/date) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [default](https://shopify.github.io/liquid/filters/default) - [x] default [Document](https://shopify.github.io/liquid/filters/default) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [divided_by](https://shopify.github.io/liquid/filters/divided_by) - [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]
- [ ] [downcase](https://shopify.github.io/liquid/filters/downcase) - [x] downcase [Document](https://shopify.github.io/liquid/filters/downcase) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [escape](https://shopify.github.io/liquid/filters/escape) - [ ] escape [Document](https://shopify.github.io/liquid/filters/escape) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [escape_once](https://shopify.github.io/liquid/filters/escape_once) - [ ] 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](https://shopify.github.io/liquid/filters/first) - [ ] first [Document](https://shopify.github.io/liquid/filters/first) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [floor](https://shopify.github.io/liquid/filters/floor) - [ ] floor [Document](https://shopify.github.io/liquid/filters/floor) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [join](https://shopify.github.io/liquid/filters/join) - [ ] join [Document](https://shopify.github.io/liquid/filters/join) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [last](https://shopify.github.io/liquid/filters/last) - [ ] last [Document](https://shopify.github.io/liquid/filters/last) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [lstrip](https://shopify.github.io/liquid/filters/lstrip) - [ ] lstrip [Document](https://shopify.github.io/liquid/filters/lstrip) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [map](https://shopify.github.io/liquid/filters/map) - [ ] map [Document](https://shopify.github.io/liquid/filters/map) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [minus](https://shopify.github.io/liquid/filters/minus) - [ ] minus [Document](https://shopify.github.io/liquid/filters/minus) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [modulo](https://shopify.github.io/liquid/filters/modulo) - [ ] 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](https://shopify.github.io/liquid/filters/newline_to_br) - [ ] 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](https://shopify.github.io/liquid/filters/plus) - [ ] plus [Document](https://shopify.github.io/liquid/filters/plus) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [prepend](https://shopify.github.io/liquid/filters/prepend) - [ ] prepend [Document](https://shopify.github.io/liquid/filters/prepend) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [remove](https://shopify.github.io/liquid/filters/remove) - [ ] remove [Document](https://shopify.github.io/liquid/filters/remove) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [remove_first](https://shopify.github.io/liquid/filters/remove_first) - [ ] 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](https://shopify.github.io/liquid/filters/replace) - [ ] replace [Document](https://shopify.github.io/liquid/filters/replace) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [replace_first](https://shopify.github.io/liquid/filters/replace_first) - [ ] 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](https://shopify.github.io/liquid/filters/reverse) - [ ] reverse [Document](https://shopify.github.io/liquid/filters/reverse) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [round](https://shopify.github.io/liquid/filters/round) - [ ] round [Document](https://shopify.github.io/liquid/filters/round) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [rstrip](https://shopify.github.io/liquid/filters/rstrip) - [ ] rstrip [Document](https://shopify.github.io/liquid/filters/rstrip) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [size](https://shopify.github.io/liquid/filters/size) - [ ] size [Document](https://shopify.github.io/liquid/filters/size) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [slice](https://shopify.github.io/liquid/filters/slice) - [ ] slice [Document](https://shopify.github.io/liquid/filters/slice) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [sort](https://shopify.github.io/liquid/filters/sort) - [ ] sort [Document](https://shopify.github.io/liquid/filters/sort) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [split](https://shopify.github.io/liquid/filters/split) - [ ] split [Document](https://shopify.github.io/liquid/filters/split) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [strip](https://shopify.github.io/liquid/filters/strip) - [ ] strip [Document](https://shopify.github.io/liquid/filters/strip) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [strip_html](https://shopify.github.io/liquid/filters/strip_html) - [ ] 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](https://shopify.github.io/liquid/filters/strip_newlines) - [ ] 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](https://shopify.github.io/liquid/filters/times) - [ ] times [Document](https://shopify.github.io/liquid/filters/times) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [truncate](https://shopify.github.io/liquid/filters/truncate) - [ ] truncate [Document](https://shopify.github.io/liquid/filters/truncate) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [truncatewords](https://shopify.github.io/liquid/filters/truncatewords) - [ ] truncatewords [Document](https://shopify.github.io/liquid/filters/truncatewords) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [uniq](https://shopify.github.io/liquid/filters/uniq) - [ ] uniq [Document](https://shopify.github.io/liquid/filters/uniq) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [upcase](https://shopify.github.io/liquid/filters/upcase) - [ ] upcase [Document](https://shopify.github.io/liquid/filters/upcase) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft]
- [ ] [url_encode](https://shopify.github.io/liquid/filters/url_encode) - [ ] 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 ## Async Support
@@ -97,3 +102,5 @@ For template-driven projects, checkout these Liquid-like engines:
[shopify-liquid]: https://shopify.github.io/liquid/ [shopify-liquid]: https://shopify.github.io/liquid/
[jekyll]: http://jekyllrb.com/ [jekyll]: http://jekyllrb.com/
[gh]: https://pages.github.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'); const _ = require('lodash');
var _filterInstance = { var _filterInstance = {
render: function(output, ctx) { render: function(output, scope) {
var args = this.args.map(arg => ctx.get(arg)); var args = this.args.map(arg => scope.get(arg));
args.unshift(output); args.unshift(output);
return this.filter.apply(null, args); return this.filter.apply(null, args);
} }
@@ -13,7 +13,7 @@ module.exports = function() {
var filters = {}; var filters = {};
function construct(str) { function construct(str) {
var match = lexical.patterns.filterLine.exec(str.trim()); var match = lexical.filterLine.exec(str.trim());
if (!match) { if (!match) {
throw new Error('illegal filter: ' + str); 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 tokenizer = require('./tokenizer.js');
const Render = require('./render.js'); const Render = require('./render.js');
const lexical = require('./lexical.js'); const lexical = require('./lexical.js');
@@ -25,10 +25,14 @@ function factory(){
engine.filter = Filter(); engine.filter = Filter();
var renderer = Render(engine.filter, engine.tag); 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.renderTokens = renderer.render;
engine.render = function(html, ctx) { 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){ fs.readdirSync(tagsPath).map(function(f){
@@ -36,12 +40,7 @@ function factory(){
if(!match) return; if(!match) return;
require("./tags/" + f)(engine); require("./tags/" + f)(engine);
}); });
require("./filters.js")(engine);
fs.readdirSync(filtersPath).map(function(f){
var match = /^(\w+)\.js$/.exec(f);
if(!match) return;
require("./filters/" + f)(engine);
});
return 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 value = new RegExp(`${literal.source}|${variable.source}`, 'i');
var hash = new RegExp(`(${identifier.source})\\s*:\\s*(${value.source})`, 'g'); var hash = 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 tagLine = new RegExp(`^\\s*(${identifier.source})\\s*(.*)\\s*$`);
var literalLine = new RegExp(`^(?:${literal.source})$`, 'i'); var literalLine = new RegExp(`^(?:${literal.source})$`, 'i');
var variableLine = new RegExp(`^(?:${variable.source})$`); var variableLine = new RegExp(`^(?:${variable.source})$`);
var numberLine = new RegExp(`^(?:${number.source})$`); var numberLine = new RegExp(`^(?:${number.source})$`);
@@ -24,20 +26,22 @@ var quotedLine = new RegExp(`^(?:${quoted.source})$`);
var rangeLine = new RegExp(`^(?:${range.source})$`); var rangeLine = new RegExp(`^(?:${range.source})$`);
var filterLine = new RegExp(`^(?:${filter.source})$`); var filterLine = new RegExp(`^(?:${filter.source})$`);
exports.patterns = { var operators = [
quoted, number, bool, range, literal, hash, filter, identifier, value, /\s+or\s+/,
quotedLine, numberLine, boolLine, rangeLine, literalLine, filterLine /\s+and\s+/,
}; /==|!=|<=|>=|<|>|\s+contains\s+/
];
exports.isLiteral = function(str) {
function isLiteral (str) {
return literalLine.test(str); return literalLine.test(str);
}; }
exports.isVariable = function(str) { function isVariable (str) {
return variableLine.test(str); return variableLine.test(str);
}; }
exports.parseLiteral = function(str) { function parseLiteral (str) {
var res; var res;
if (res = str.match(numberLine)) { if (res = str.match(numberLine)) {
return Number(str); return Number(str);
@@ -51,4 +55,12 @@ exports.parseLiteral = function(str) {
if (res = str.match(rangeLine)) { if (res = str.match(rangeLine)) {
return _.range(res[1], res[2]); 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", "name": "shopify-liquid",
"version": "1.0.0", "version": "1.0.1",
"description": "A Shopify Liquid Implementation in Node.js", "description": "A Shopify Liquid Implementation in Node.js",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
+34 -14
View File
@@ -1,5 +1,8 @@
const lexical = require('./lexical.js');
const syntax = require('./syntax.js');
module.exports = function(Filter, Tag) { module.exports = function(Filter, Tag) {
function render(tokens, ctx) { function render(tokens, scope) {
var html = ''; var html = '';
while (tokens.length) { while (tokens.length) {
var token = tokens.shift(); var token = tokens.shift();
@@ -8,10 +11,10 @@ module.exports = function(Filter, Tag) {
html += token.value; html += token.value;
break; break;
case 'output': case 'output':
html += evaluate(token.value, ctx); html += evalFilter(token.value, scope);
break; break;
case 'tag': case 'tag':
html += renderTag(token, tokens, ctx); html += renderTag(token, tokens, scope);
break; break;
default: default:
throw new Error(`unexpected type: ${token.type}`); throw new Error(`unexpected type: ${token.type}`);
@@ -20,20 +23,37 @@ module.exports = function(Filter, Tag) {
return html; return html;
} }
function evaluate(str, ctx) { function evalExp(exp, scope) {
if(!ctx) throw new Error('ctx is needed to evaluate'); if(!scope) throw new Error('unable to evalExp: scope undefined');
var filters = str.split('|'); var operatorREs = lexical.operators;
var val = ctx.get(filters.shift()); for (var i = 0; i < operatorREs.length; i++) {
return filters var operatorRE = operatorREs[i];
.map(str => Filter.construct(str)) var expRE = new RegExp(`^(${lexical.quoteBalanced.source})(${operatorRE.source})(${lexical.quoteBalanced.source})$`);
.reduce((v, filter) => filter.render(v, ctx), val); 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), var tag = Tag.construct(token),
subTokens = []; subTokens = [];
if (tag.needClose) { if (tag.needClose) {
var curToken, endToken = 'end' + tag.name; var curToken, endToken = 'end' + tag.token.name;
while ((curToken = tokens.shift()) && curToken.value !== endToken) { while ((curToken = tokens.shift()) && curToken.value !== endToken) {
subTokens.push(curToken); subTokens.push(curToken);
} }
@@ -41,10 +61,10 @@ module.exports = function(Filter, Tag) {
throw new Error(`${token.value} not closed`); throw new Error(`${token.value} not closed`);
} }
} }
return tag.render(subTokens, ctx); return tag.render(subTokens, scope);
} }
return { return {
render, evaluate, renderTag render, renderTag, evalFilter, evalExp
}; };
}; };
+8 -8
View File
@@ -1,7 +1,7 @@
const _ = require('lodash'); const _ = require('lodash');
const lexical = require('./lexical.js'); const lexical = require('./lexical.js');
var context = { var scope = {
get: function(str) { get: function(str) {
str = str && str.trim(); str = str && str.trim();
if(!str) return ''; if(!str) return '';
@@ -11,27 +11,27 @@ var context = {
return lexical.parseLiteral(str); return lexical.parseLiteral(str);
} }
if (lexical.isVariable(str)) { if (lexical.isVariable(str)) {
for (var i = this.context.length - 1; i >= 0; i--) { for (var i = this.scopes.length - 1; i >= 0; i--) {
var v = _.get(this.context[i], str); var v = _.get(this.scopes[i], str);
if (v !== undefined) return v; if (v !== undefined) return v;
} }
} }
return ''; return '';
}, },
set: function(k, v){ set: function(k, v){
this.context[this.context.length-1][k] = v; this.scopes[this.scopes.length-1][k] = v;
return this; return this;
}, },
push: function(ctx) { push: function(ctx) {
return this.context.push(ctx); return this.scopes.push(ctx);
}, },
pop: function() { pop: function() {
return this.context.pop(); return this.scopes.pop();
} }
}; };
exports.factory = function(_ctx) { exports.factory = function(_ctx) {
var ctx = Object.create(context); var ctx = Object.create(scope);
ctx.context = [_ctx]; ctx.scopes = [_ctx];
return 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'); const lexical = require('./lexical.js');
var _tagInstance = { var _tagInstance = {
render: function(tokens, ctx) { render: function(tokens, scope) {
var obj = hash(this.markup, ctx); var obj = hash(this.token.args, scope);
var res = this.tag.render(tokens, ctx, this.markup, obj); var res = this.tag.render(tokens, scope, this.token, obj);
return res === undefined ? '' : res; return res === undefined ? '' : res;
} }
}; };
function hash(markup, ctx) { function hash(markup, scope) {
var obj = {}; var obj = {};
lexical.patterns.hash.lastIndex = 0; lexical.hash.lastIndex = 0;
while (match = lexical.patterns.hash.exec(markup)) { while (match = lexical.hash.exec(markup)) {
var k = match[1], var k = match[1],
v = match[2]; v = match[2];
if (!k) continue; if (!k) continue;
obj[k] = ctx.get(v); obj[k] = scope.get(v);
} }
return obj; return obj;
} }
@@ -35,8 +35,7 @@ module.exports = function() {
if (!tag) throw new Error(`tag ${token.name} not found`); if (!tag) throw new Error(`tag ${token.name} not found`);
var instance = Object.create(_tagInstance); var instance = Object.create(_tagInstance);
instance.name = token.name; instance.token = token;
instance.markup = token.value;
instance.tag = tag; instance.tag = tag;
instance.needClose = tag.needClose; instance.needClose = tag.needClose;
return instance; return instance;
+5 -5
View File
@@ -1,14 +1,14 @@
var Liquid = require('..'); var Liquid = require('..');
var patterns = Liquid.lexical.patterns; var lexical = Liquid.lexical;
var re = new RegExp(`(${patterns.identifier.source})\\s*=(.*)`); 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, ctx, markup, hash) { render: function(tokens, scope, token, hash) {
var match = markup.match(re); var match = token.value.match(re);
var k = match[1], v = match[2]; 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 Liquid = require('..');
var patterns = Liquid.lexical.patterns; var lexical = Liquid.lexical;
var caseRE = new RegExp(`^\\s*case\\s+(${patterns.value.source})`); var caseRE = new RegExp(`^\\s*case\\s+(${lexical.value.source})`);
var whenRE = new RegExp(`^\\s*when\\s+(${patterns.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, needClose: true,
render: function(tokens, ctx, markup, hash) { render: function(tokens, scope, token, hash) {
var match = markup.match(caseRE); var match = token.value.match(caseRE);
var cond = liquid.evaluate(match[1], ctx); var cond = liquid.evaluate(match[1], scope);
var partialTokens = [], var partialTokens = [],
matching = false; matching = false;
for (var i = 0; i < tokens.length; i++) { for (var i = 0; i < tokens.length; i++) {
var token = tokens[i]; var tk = tokens[i];
if (token.type === 'tag' && token.name === 'when') { if (tk.type === 'tag' && tk.name === 'when') {
if (matching) break; if (matching) break;
match = token.value.match(whenRE); match = tk.value.match(whenRE);
if(!match) continue; if(!match) continue;
var val = liquid.evaluate(match[1], ctx); var val = liquid.evaluate(match[1], scope);
if (val === cond) matching = true; if (val === cond) matching = true;
} else if (token.type === 'tag' && token.name === 'else') { } else if (tk.type === 'tag' && tk.name === 'else') {
if (matching) break; if (matching) break;
else matching = true; 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); chai.use(sinonChai);
var filter = require('../filter.js')(); var filter = require('../filter.js')();
var context = require('../context.js'); var Scope = require('../scope.js');
describe('filter', function() { describe('filter', function() {
var ctx; var scope;
beforeEach(function(){ beforeEach(function(){
filter.clear(); filter.clear();
ctx = context.factory(); scope = Scope.factory();
}); });
it('should throw when not registered', function() { it('should throw when not registered', function() {
expect(function() { expect(function() {
@@ -22,13 +22,13 @@ describe('filter', function() {
it('should register a simple filter', function(){ it('should register a simple filter', function(){
filter.register('foo', x => x.toUpperCase()); 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(){ it('should call filter with corrct arguments', function(){
var spy = sinon.spy(); var spy = sinon.spy();
filter.register('foo', 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); expect(spy).to.have.been.calledWith('foo', 33);
}); });
}); });
+38 -1
View File
@@ -5,12 +5,49 @@ var liquid = require('..')();
describe('filters', function() { describe('filters', function() {
var ctx = { var ctx = {
date: new Date('1995-12-17T03:24:00'),
foo: 'bar', foo: 'bar',
arr: [-2, 'a'] arr: [-2, 'a']
}; };
it('should support abs', function() { 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'); 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); chai.use(sinonChai);
var tag = require('../tag.js')(); var tag = require('../tag.js')();
var context = require('../context.js'); var Scope = require('../scope.js');
var filter = require('../filter')(); 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() { describe('render', function() {
var ctx, htmlToken, tagToken, filterToken; var scope, htmlToken, tagToken, filterToken;
before(function() { before(function() {
ctx = context.factory({ scope = Scope.factory({
one: 1,
two: 2,
x: 'XXX', x: 'XXX',
foo: { foo: {
bar: ['a', 2] bar: ['a', 2]
@@ -23,7 +28,8 @@ describe('render', function() {
tagToken = { tagToken = {
type: 'tag', type: 'tag',
value: 'foo bar:x foo:"FOO" num:2.3', value: 'foo bar:x foo:"FOO" num:2.3',
name: 'foo' name: 'foo',
args: 'bar:x foo:"FOO" num:2.3'
}; };
htmlToken = { htmlToken = {
type: 'html', type: 'html',
@@ -41,21 +47,21 @@ describe('render', function() {
}); });
it('should render html', 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() { it('should render with tag function', function() {
tag.register('foo', { tag.register('foo', {
render: x => 'X' 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() { it('should call tag with correct arguments', function() {
var spy = sinon.spy(); var spy = sinon.spy();
tag.register('foo', { render: spy }); tag.register('foo', { render: spy });
render([tagToken], ctx); render([tagToken], scope);
expect(spy).to.have.been.calledWithMatch([], ctx, tagToken.value, { expect(spy).to.have.been.calledWithMatch([], scope, tagToken, {
bar: 'XXX', bar: 'XXX',
foo: 'FOO', foo: 'FOO',
num: 2.3 num: 2.3
@@ -65,7 +71,7 @@ describe('render', function() {
it('should render with filter function', function() { it('should render with filter function', function() {
filter.register('date', (l, r) => l + r); filter.register('date', (l, r) => l + r);
filter.register('time', (l, r) => l + 3*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() { it('should call filter with correct arguments', function() {
@@ -73,8 +79,23 @@ describe('render', function() {
var time = sinon.spy(); var time = sinon.spy();
filter.register('date', date); filter.register('date', date);
filter.register('time', time); filter.register('time', time);
render([filterToken], ctx); render([filterToken], scope);
expect(date).to.have.been.calledWith('a', 'b'); expect(date).to.have.been.calledWith('a', 'b');
expect(time).to.have.been.calledWith('y', 2); 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); chai.use(sinonChai);
var tag = require('../tag.js')(); var tag = require('../tag.js')();
var context = require('../context.js'); var Scope = require('../scope.js');
describe('tag', function() { describe('tag', function() {
var ctx; var scope;
before(function() { before(function() {
ctx = context.factory({ scope = Scope.factory({
foo: 'bar', foo: 'bar',
arr: [2, 1] arr: [2, 1]
}); });
@@ -52,7 +52,7 @@ describe('tag', function() {
type: 'tag', type: 'tag',
value: 'foo', value: 'foo',
name: 'foo' name: 'foo'
}).render(tokens, ctx); }).render(tokens, scope);
expect(spy).to.have.been.called; expect(spy).to.have.been.called;
}); });
@@ -62,13 +62,14 @@ describe('tag', function() {
tag.register('foo', { tag.register('foo', {
render: spy render: spy
}); });
var t = tag.construct({ var token = {
type: 'tag', type: 'tag',
value: 'foo aa:foo bb: arr[0] cc: 2.3', value: 'foo aa:foo bb: arr[0] cc: 2.3',
name: 'foo' name: 'foo',
}); args: 'aa:foo bb: arr[0] cc: 2.3'
t.render(tokens, ctx); };
expect(spy).to.have.been.calledWithMatch(tokens, ctx, 'foo', { tag.construct(token).render(tokens, scope);
expect(spy).to.have.been.calledWithMatch(tokens, scope, token, {
aa: 'bar', aa: 'bar',
bb: 2, bb: 2,
cc: 2.3 cc: 2.3
+14
View File
@@ -4,6 +4,9 @@ const expect = chai.expect;
var liquid = require('..')(); var liquid = require('..')();
var ctx = { var ctx = {
one: 1,
two: 2,
leq: '<=',
empty: '', empty: '',
foo: 'bar', foo: 'bar',
arr: [-2, 'a'] arr: [-2, 'a']
@@ -30,5 +33,16 @@ describe('tags', function() {
'{% when "b" %}b{% when "c"%}c{%else %}d' + '{% when "b" %}b{% when "c"%}c{%else %}d' +
'{%endcase%}')).to.equal('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 value: htmlFragment
}); });
} }
if(result[1]){ var rawTag = result[1],
var match = result[1].trim().match(lexical.patterns.identifier); cleanTag = result[2],
if (!match) throw new Error('illegal tag: ' + result[1]); 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({ tokens.push({
type: 'tag', type: 'tag',
name: match[0], value: cleanTag.trim(),
value: result[2].trim() raw: rawTag,
args, name
}); });
} }
else{ else{
tokens.push({ tokens.push({
type: 'output', type: 'output',
value: result[4].trim() raw: rawOut,
value: cleanOut.trim()
}); });
} }
idx = syntax.lastIndex; idx = syntax.lastIndex;