mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-15 04:10:40 -07:00
render, filter, tag
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
## Async Support
|
||||
|
||||
harttle/shopify-liquid do NOT support async rendering, this is by design.
|
||||
|
||||
The primary principle of harttle/shopify-liquid is EASY TO EXTEND.
|
||||
Async rendering introduces extra complexity in both implementation and extension.
|
||||
|
||||
For template-driven projects, checkout these Liquid-like engines:
|
||||
|
||||
* [liquid-node][liquid-node]: <https://github.com/sirlantis/liquid-node>
|
||||
* [nunjucks][nunjucks]: <http://mozilla.github.io/nunjucks/>
|
||||
|
||||
[nunjucks]: http://mozilla.github.io/nunjucks/
|
||||
[liquid-node]: https://github.com/sirlantis/liquid-node
|
||||
+20
-13
@@ -1,26 +1,33 @@
|
||||
const _ = require('lodash');
|
||||
const identifier = require('./identifier.js');
|
||||
const lexical = require('./lexical.js');
|
||||
|
||||
var context = {
|
||||
get: function(str){
|
||||
if(identifier.isLiteral(str)){
|
||||
return identifier.parseLiteral(str);
|
||||
}
|
||||
if(identifier.isVariable(str)){
|
||||
return _.get(this.context, str);
|
||||
get: function(str) {
|
||||
str = str && str.trim();
|
||||
if(!str) return '';
|
||||
|
||||
if (lexical.isLiteral(str)) {
|
||||
var a = lexical.parseLiteral(str);
|
||||
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);
|
||||
if (v !== undefined) return v;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
},
|
||||
init: function(ctx){
|
||||
this.context = ctx;
|
||||
push: function(ctx) {
|
||||
return this.context.push(ctx);
|
||||
},
|
||||
merge: function(ctx){
|
||||
_.merge(this.context, ctx);
|
||||
pop: function() {
|
||||
return this.context.pop();
|
||||
}
|
||||
};
|
||||
|
||||
exports.factory = function(_ctx){
|
||||
exports.factory = function(_ctx) {
|
||||
var ctx = Object.create(context);
|
||||
ctx.init(_ctx);
|
||||
ctx.context = [_ctx];
|
||||
return ctx;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
const lexical = require('./lexical.js');
|
||||
const _ = require('lodash');
|
||||
|
||||
var filters = {};
|
||||
|
||||
var _filterInstance = {
|
||||
render: function(output, ctx) {
|
||||
var args = this.args.map(arg => ctx.get(arg));
|
||||
args.unshift(output);
|
||||
return this.filter.apply(null, args);
|
||||
}
|
||||
}
|
||||
|
||||
function parse(str) {
|
||||
var match = lexical.patterns.filterLine.exec(str.trim());
|
||||
if (!match) {
|
||||
throw new Error('illegal filter: ' + str);
|
||||
}
|
||||
var k = match[1],
|
||||
v = match[2];
|
||||
|
||||
return factory(k, [v]);
|
||||
}
|
||||
|
||||
function factory(name, args) {
|
||||
var filter = filters[name];
|
||||
if (typeof filter !== 'function')
|
||||
throw new Error(`filter ${name} not found`);
|
||||
|
||||
var instance = Object.create(_filterInstance);
|
||||
instance.args = args;
|
||||
instance.filter = filter;
|
||||
return instance;
|
||||
}
|
||||
|
||||
function register(name, filter) {
|
||||
filters[name] = filter;
|
||||
}
|
||||
|
||||
function clear() {
|
||||
filters = {};
|
||||
}
|
||||
|
||||
exports.parse = parse;
|
||||
exports.register = register;
|
||||
exports.clear = clear;
|
||||
@@ -1,41 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
var singleQuoted = /^'[^']*'$/;
|
||||
var doubleQuoted = /^"[^"]*"$/;
|
||||
|
||||
var number = /^(?:\d+\.?\d*|\.?\d+)$/;
|
||||
var bool = /^(?:true|false)$/i;
|
||||
var range = /^\((\d+)\.\.(\d+)\)$/;
|
||||
|
||||
var quoted = new RegExp(`${singleQuoted.source}|${doubleQuoted.source}`);
|
||||
var literal = new RegExp(`${quoted.source}|${range.source}|${bool.source}|${number.source}`, 'i');
|
||||
var variable = /^[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*|\[\d+\])*$/;
|
||||
var identifier = new RegExp(`${literal.source}|${variable.source}`, 'i');
|
||||
|
||||
exports.patterns = {
|
||||
quoted, number, bool, range, literal
|
||||
};
|
||||
|
||||
exports.isLiteral = function(str) {
|
||||
return literal.test(str);
|
||||
};
|
||||
|
||||
exports.isVariable = function(str) {
|
||||
return variable.test(str);
|
||||
};
|
||||
|
||||
exports.parseLiteral = function(str) {
|
||||
var res;
|
||||
if(res = str.match(number)){
|
||||
return Number(str);
|
||||
}
|
||||
if(res = str.match(bool)){
|
||||
return str.toLowerCase() === 'true';
|
||||
}
|
||||
if(res = str.match(quoted)){
|
||||
return str.slice(1, -1);
|
||||
}
|
||||
if(res = str.match(range)){
|
||||
return _.range(res[1], res[2]);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
const context = require('./context');
|
||||
const tokenizer = require('./tokenizer.js');
|
||||
const render = require('./render.js');
|
||||
const lexical = require('./lexical.js');
|
||||
|
||||
exports.render = function(html, ctx){
|
||||
return render(tokenizer(html), context.factory(ctx));
|
||||
};
|
||||
|
||||
exports.lexical = lexical;
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
var singleQuoted = /'[^']*'/;
|
||||
var doubleQuoted = /"[^"]*"/;
|
||||
|
||||
var number = /\d+\.?\d*|\.?\d+/;
|
||||
var bool = /true|false/i;
|
||||
var range = /\((\d+)\.\.(\d+)\)/;
|
||||
var identifier = /[a-zA-Z_$][a-zA-Z_$0-9]*/;
|
||||
var subscript = /\[\d+\]/;
|
||||
|
||||
var quoted = new RegExp(`${singleQuoted.source}|${doubleQuoted.source}`);
|
||||
var literal = new RegExp(`${quoted.source}|${range.source}|${bool.source}|${number.source}`, 'i');
|
||||
var variable = new RegExp(`${identifier.source}(?:\\.${identifier.source}|${subscript.source})*`);
|
||||
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 literalLine = new RegExp(`^(?:${literal.source})$`, 'i');
|
||||
var variableLine = new RegExp(`^(?:${variable.source})$`);
|
||||
var numberLine = new RegExp(`^(?:${number.source})$`);
|
||||
var boolLine = new RegExp(`^(?:${bool.source})$`, 'i');
|
||||
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,
|
||||
quotedLine, numberLine, boolLine, rangeLine, literalLine, filterLine
|
||||
};
|
||||
|
||||
exports.isLiteral = function(str) {
|
||||
return literalLine.test(str);
|
||||
};
|
||||
|
||||
exports.isVariable = function(str) {
|
||||
return variableLine.test(str);
|
||||
};
|
||||
|
||||
exports.parseLiteral = function(str) {
|
||||
var res;
|
||||
if (res = str.match(numberLine)) {
|
||||
return Number(str);
|
||||
}
|
||||
if (res = str.match(boolLine)) {
|
||||
return str.toLowerCase() === 'true';
|
||||
}
|
||||
if (res = str.match(quotedLine)) {
|
||||
return str.slice(1, -1);
|
||||
}
|
||||
if (res = str.match(rangeLine)) {
|
||||
return _.range(res[1], res[2]);
|
||||
}
|
||||
};
|
||||
+2
-3
@@ -21,15 +21,14 @@
|
||||
},
|
||||
"homepage": "https://github.com/harttle/shopify-liquid#readme",
|
||||
"dependencies": {
|
||||
"bluebird": "^3.4.0",
|
||||
"lodash": "^4.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^3.5.0",
|
||||
"chai-as-promised": "^5.3.0",
|
||||
"coveralls": "^2.11.9",
|
||||
"istanbul": "^0.4.3",
|
||||
"mocha": "^2.5.3",
|
||||
"sinon": "^1.17.4"
|
||||
"sinon": "^1.17.4",
|
||||
"sinon-chai": "^2.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
const Filter = require('./filter');
|
||||
const Tag = require('./tag');
|
||||
|
||||
module.exports = function render(tokens, ctx) {
|
||||
var html = '';
|
||||
for (var i = 0; i < tokens.length; i++) {
|
||||
var token = tokens.shift();
|
||||
switch (token.type) {
|
||||
case 'html':
|
||||
html += token.value;
|
||||
break;
|
||||
case 'output':
|
||||
html += renderOutput(token);
|
||||
break;
|
||||
case 'tag':
|
||||
html += renderTag(token);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`unexpected type: ${token.type}`);
|
||||
}
|
||||
}
|
||||
return html;
|
||||
|
||||
function renderOutput(token) {
|
||||
var filters = token.value.split('|');
|
||||
var val = ctx.get(filters.shift());
|
||||
return filters
|
||||
.map(str => Filter.parse(str))
|
||||
.reduce((v, filter) => filter.render(v, ctx), val);
|
||||
}
|
||||
|
||||
function renderTag(token) {
|
||||
var tag = Tag.parse(token.value), subTokens = [];
|
||||
if(tag.needClose){
|
||||
var curToken, endToken = 'end' + tag.name;
|
||||
while((curToken = tokens.shift()).value !== endToken){
|
||||
subTokens.push(curToken);
|
||||
}
|
||||
if(curToken.value !== endToken){
|
||||
throw new Error(`${token.value} not closed`);
|
||||
}
|
||||
}
|
||||
return tag.render(subTokens, ctx);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
const lexical = require('./lexical.js');
|
||||
const context = require('./context.js');
|
||||
|
||||
var tags = {};
|
||||
|
||||
var _tagInstance = {
|
||||
render: function(tokens, ctx){
|
||||
var obj = hash(this.markup, ctx);
|
||||
return this.tag.render(tokens, ctx, this.markup, obj);
|
||||
}
|
||||
};
|
||||
|
||||
function register(name, tag){
|
||||
if(typeof tag.render !== 'function'){
|
||||
throw new Error(`expect ${name}.render to be a function`);
|
||||
}
|
||||
tags[name] = tag;
|
||||
}
|
||||
|
||||
function parse(str){
|
||||
var match = lexical.patterns.identifier.exec(str.trim());
|
||||
if(!match) throw new Error('illegal tag: '+ str);
|
||||
|
||||
var tagInstance = factory(match[0], str);
|
||||
return tagInstance;
|
||||
}
|
||||
|
||||
function hash(markup, ctx){
|
||||
var obj = {};
|
||||
lexical.patterns.hash.lastIndex = 0;
|
||||
while(match = lexical.patterns.hash.exec(markup)){
|
||||
var k = match[1], v = match[2];
|
||||
if(!k) continue;
|
||||
obj[k] = ctx.get(v);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
function factory(name, markup){
|
||||
var tag = tags[name];
|
||||
if(!tag) throw new Error(`tag ${name} not found`);
|
||||
|
||||
var instance = Object.create(_tagInstance);
|
||||
instance.name = name;
|
||||
instance.markup = markup;
|
||||
instance.tag = tag;
|
||||
return instance;
|
||||
}
|
||||
|
||||
function clear(){
|
||||
tags = {};
|
||||
}
|
||||
|
||||
exports.parse = parse;
|
||||
exports.register = register;
|
||||
exports.hash = hash;
|
||||
exports.clear = clear;
|
||||
+11
-3
@@ -1,6 +1,6 @@
|
||||
var chai = require("chai");
|
||||
var should = chai.should();
|
||||
chai.use(require("chai-as-promised"));
|
||||
var expect = chai.expect;
|
||||
|
||||
var context = require('../context.js');
|
||||
|
||||
@@ -29,9 +29,17 @@ describe('context', function() {
|
||||
ctx.get('bar[1].b[1]').should.equal(2);
|
||||
});
|
||||
|
||||
it('should merge context', function() {
|
||||
ctx.merge({foo: 'foo', foo1: 'foo1'});
|
||||
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.pop();
|
||||
expect(ctx.get('foo')).to.equal('bar');
|
||||
expect(ctx.get('foo1')).to.equal('');
|
||||
expect(ctx.get('bar[1].b[1]')).to.equal(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
const chai = require("chai");
|
||||
const sinon = require("sinon");
|
||||
const sinonChai = require("sinon-chai");
|
||||
const expect = chai.expect;
|
||||
|
||||
chai.use(sinonChai);
|
||||
|
||||
var filter = require('../filter.js');
|
||||
var context = require('../context.js');
|
||||
|
||||
describe('filter', function() {
|
||||
var ctx;
|
||||
beforeEach(function(){
|
||||
filter.clear();
|
||||
ctx = context.factory();
|
||||
});
|
||||
it('should throw when not registered', function() {
|
||||
expect(function() {
|
||||
filter.parse('foo');
|
||||
}).to.throw(/filter foo not found/);
|
||||
});
|
||||
|
||||
it('should register a simple filter', function(){
|
||||
filter.register('foo', x => x.toUpperCase());
|
||||
expect(filter.parse('foo').render('foo', ctx)).to.equal('FOO');
|
||||
});
|
||||
|
||||
it('should call filter with corrct arguments', function(){
|
||||
var spy = sinon.spy();
|
||||
filter.register('foo', spy);
|
||||
filter.parse('foo: 33').render('foo', ctx);
|
||||
expect(spy).to.have.been.calledWith('foo', 33);
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ var chai = require("chai");
|
||||
var should = chai.should();
|
||||
chai.use(require("chai-as-promised"));
|
||||
|
||||
var identifier = require('../identifier.js');
|
||||
var identifier = require('../lexical.js');
|
||||
|
||||
describe('identifier', function() {
|
||||
it('should test boolean literal', function() {
|
||||
@@ -0,0 +1,79 @@
|
||||
const chai = require("chai");
|
||||
const sinonChai = require("sinon-chai");
|
||||
const sinon = require("sinon");
|
||||
const expect = chai.expect;
|
||||
|
||||
chai.use(sinonChai);
|
||||
|
||||
var tag = require('../tag.js');
|
||||
var context = require('../context.js');
|
||||
var filter = require('../filter');
|
||||
var render = require('../render.js');
|
||||
|
||||
describe('render', function() {
|
||||
var ctx, htmlToken, tagToken, filterToken;
|
||||
|
||||
before(function() {
|
||||
ctx = context.factory({
|
||||
x: 'XXX',
|
||||
foo: {
|
||||
bar: ['a', 2]
|
||||
}
|
||||
});
|
||||
tagToken = {
|
||||
type: 'tag',
|
||||
value: 'foo bar:x foo:"FOO" num:2.3'
|
||||
};
|
||||
htmlToken = {
|
||||
type: 'html',
|
||||
value: '<p>'
|
||||
};
|
||||
filterToken = {
|
||||
type: 'output',
|
||||
value: 'foo.bar[0] | date: "b" | time:2'
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(function(){
|
||||
filter.clear();
|
||||
tag.clear();
|
||||
});
|
||||
|
||||
it('should render html', function() {
|
||||
expect(render([htmlToken], ctx)).to.equal('<p>');
|
||||
});
|
||||
|
||||
it('should render with tag function', function() {
|
||||
tag.register('foo', {
|
||||
render: x => 'X'
|
||||
});
|
||||
expect(render([tagToken], ctx)).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, {
|
||||
bar: 'XXX',
|
||||
foo: 'FOO',
|
||||
num: 2.3
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
it('should call filter with correct arguments', function() {
|
||||
var date = sinon.stub().returns('y');
|
||||
var time = sinon.spy();
|
||||
filter.register('date', date);
|
||||
filter.register('time', time);
|
||||
render([filterToken], ctx);
|
||||
expect(date).to.have.been.calledWith('a', 'b');
|
||||
expect(time).to.have.been.calledWith('y', 2);
|
||||
});
|
||||
});
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
var chai = require("chai");
|
||||
var sinonChai = require("sinon-chai");
|
||||
var sinon = require("sinon");
|
||||
var expect = chai.expect;
|
||||
chai.use(sinonChai);
|
||||
|
||||
var tag = require('../tag.js');
|
||||
var context = require('../context.js');
|
||||
|
||||
describe('tag', function() {
|
||||
var ctx;
|
||||
before(function(){
|
||||
ctx = context.factory({
|
||||
foo: 'bar',
|
||||
arr: [2, 1]
|
||||
});
|
||||
tag.clear();
|
||||
});
|
||||
|
||||
it('should throw when not registered', function() {
|
||||
expect(function() {
|
||||
tag.parse('foo');
|
||||
}).to.throw(/tag foo not found/);
|
||||
});
|
||||
|
||||
it('should throw when render method not defined', function() {
|
||||
expect(function() {
|
||||
tag.register('foo', {});
|
||||
}).to.throw(/expect foo.render to be a function/);
|
||||
});
|
||||
|
||||
it('should register simple tag', function() {
|
||||
expect(
|
||||
function() {
|
||||
tag.register('foo', {
|
||||
render: x => 'bar'
|
||||
});
|
||||
}).not.throw();
|
||||
});
|
||||
|
||||
it('should call tag.render', function() {
|
||||
var spy = sinon.spy(),
|
||||
tokens = [];
|
||||
tag.register('foo', {
|
||||
render: spy
|
||||
});
|
||||
tag.parse('foo').render(tokens, ctx);
|
||||
expect(spy).to.have.been.called;
|
||||
});
|
||||
|
||||
it('should call tag.render with resolved hash', function() {
|
||||
var spy = sinon.spy(),
|
||||
tokens = [];
|
||||
tag.register('foo', {
|
||||
render: spy
|
||||
});
|
||||
var t = tag.parse('foo aa:foo bb: arr[0] cc: 2.3');
|
||||
t.render(tokens, ctx);
|
||||
expect(spy).to.have.been.calledWithMatch(tokens, ctx, 'foo', {
|
||||
aa: 'bar',
|
||||
bb: 2,
|
||||
cc: 2.3
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
var chai = require("chai");
|
||||
var should = chai.should();
|
||||
chai.use(require("chai-as-promised"));
|
||||
|
||||
var tokenizer = require('../tokenizer.js');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user