enable cache: parse and render

This commit is contained in:
harttle
2016-06-20 00:34:54 +08:00
parent aa1df95714
commit 54bc206d36
16 changed files with 182 additions and 118 deletions
+52
View File
@@ -11,6 +11,58 @@ shall be implemented.
> [Shopify liquid][shopify-liquid] is used by [Jekyll][jekyll] and [Github Pages][gh]. > [Shopify liquid][shopify-liquid] is used by [Jekyll][jekyll] and [Github Pages][gh].
## Usage
Install:
```bash
npm install --save shopify-liquid
```
Parse and Render:
```javascript
var Liquid = require('shopify-liquid');
var engine = Liquid();
engine.parseAndRender('{{name | capitalize}}', {name: 'alice'}); // Alice
```
Caching templates:
```javascript
var tpl = engine.parse('{{name | capitalize}}');
engine.render(tpl, {name: 'alice'}); // Alice
```
Register Filters:
```javascript
// Usage: {{ name | uppper }}
engine.registerFilter('upper', function(v){
return v.toUpperCase();
});
```
> See existing filter implementations: <https://github.com/harttle/shopify-liquid/blob/master/filters.js>
Register Tags:
```javascript
// Usage: {% upper name%}
engine.registerTag('upper', {
parse: function(tagToken, remainTokens) {
this.str = tagToken.args; // name
},
render: function(scope, hash) {
var str = Liquid.evalValue(this.str, scope); // 'alice'
return str.toUpperCase(); // 'Alice'
}
});
```
> See existing tag implementations: <https://github.com/harttle/shopify-liquid/blob/master/tags/>
## Operators ## Operators
Documentation: <https://shopify.github.io/liquid/basics/operators/> Documentation: <https://shopify.github.io/liquid/basics/operators/>
+39 -38
View File
@@ -6,61 +6,62 @@ const path = require("path");
const fs = require('fs'); const fs = require('fs');
const Tag = require('./tag.js'); const Tag = require('./tag.js');
const Filter = require('./filter.js'); const Filter = require('./filter.js');
const error = require('./error.js'); const Template = require('./parser');
const Template = require('./template');
const Expression = require('./expression.js'); const Expression = require('./expression.js');
const tagsPath = path.join(__dirname, "tags"); const tagsPath = path.join(__dirname, "tags");
var _engine = { var _engine = {
registerFilter : function(name, filter){ init: function(tag, filter) {
this.tag = tag;
this.filter = filter;
this.parser = Template(tag, filter);
this.renderer = Render();
return this;
},
parse: function(html) {
var tokens = tokenizer.parse(html);
return this.parser.parse(tokens);
},
render: function(tpl, ctx) {
this.renderer.resetRegisters();
return this.renderer.renderTemplates(tpl, scope.factory(ctx));
},
parseAndRender: function(html, ctx) {
var tpl = this.parse(html);
return this.render(tpl, ctx);
},
evalOutput: function(str, scope) {
var tpl = this.parser.parseOutput(str.trim());
return this.renderer.evalOutput(tpl, scope);
},
registerFilter: function(name, filter) {
return this.filter.register(name, filter); return this.filter.register(name, filter);
}, },
registerTag : function(name, tag){ registerTag: function(name, tag) {
return this.tag.register(name, tag); return this.tag.register(name, tag);
} },
}; };
function factory(){ function factory() {
var engine = Object.create(_engine); var engine = Object.create(_engine);
engine.init(Tag(), Filter());
engine.tag = Tag(); registerTagsAndFilters(engine);
engine.filter = Filter();
engine.tokenize = tokenizer.parse;
engine.template = Template(engine.tag, engine.filter);
engine.parseStream = engine.template.parseStream;
var renderer = Render(engine.filter, engine.tag);
engine.renderTemplates = renderer.renderTemplates;
engine.render = function(html, ctx) {
var tokens = engine.tokenize(html);
var templates = engine.template.parse(tokens);
engine.register = {};
return engine.renderTemplates(templates, scope.factory(ctx));
};
engine.evalOutput = function(str, scope) {
var template = engine.template.parseOutput(str.trim());
return renderer.evalOutput(template, scope);
};
fs.readdirSync(tagsPath).map(function(f){
var match = /^(\w+)\.js$/.exec(f);
if(!match) return;
require("./tags/" + f)(engine);
});
require("./filters.js")(engine);
return engine; return engine;
} }
function registerTagsAndFilters(engine) {
fs.readdirSync(tagsPath).map(f => {
var match = /^(\w+)\.js$/.exec(f);
if (!match) return;
require("./tags/" + f)(engine);
});
require("./filters.js")(engine);
}
factory.lexical = lexical; factory.lexical = lexical;
factory.error = error;
factory.isTruthy = Expression.isTruthy; factory.isTruthy = Expression.isTruthy;
factory.isFalsy = Expression.isFalsy; factory.isFalsy = Expression.isFalsy;
factory.stringify = Render.stringify;
factory.evalExp = Expression.evalExp; factory.evalExp = Expression.evalExp;
factory.evalValue = Expression.evalValue; factory.evalValue = Expression.evalValue;
module.exports = factory; module.exports = factory;
View File
+25 -20
View File
@@ -2,36 +2,32 @@ const error = require('./error.js');
const Exp = require('./expression.js'); const Exp = require('./expression.js');
const assert = require('assert'); const assert = require('assert');
function stringify(val) { var render = {
if (typeof val === 'string') return val;
return JSON.stringify(val);
}
function factory(Filter, Tag) { renderTemplates: function(templates, scope) {
function renderTemplates(templates, scope) {
assert(scope, 'unable to evalTemplates: scope undefined'); assert(scope, 'unable to evalTemplates: scope undefined');
var html = '', partial; var html = '',
partial;
templates.some(template => { templates.some(template => {
if (scope.get('forloop.skip')) return true; if (scope.get('forloop.skip')) return true;
switch (template.type) { switch (template.type) {
case 'tag': case 'tag':
partial = renderTag(template, scope, this.register); partial = this.renderTag(template, scope, this.register);
if(partial === undefined) return true; if (partial === undefined) return true;
html += partial; html += partial;
break; break;
case 'html': case 'html':
html += template.value; html += template.value;
break; break;
case 'output': case 'output':
var val = evalOutput(template, scope); var val = this.evalOutput(template, scope);
html += stringify(val); html += stringify(val);
} }
}); });
return html; return html;
} },
function renderTag(template, scope, register) { renderTag: function(template, scope, register) {
if (template.name === 'continue') { if (template.name === 'continue') {
scope.set('forloop.skip', true); scope.set('forloop.skip', true);
return; return;
@@ -42,20 +38,29 @@ function factory(Filter, Tag) {
return; return;
} }
return template.render(scope, register); return template.render(scope, register);
} },
function evalOutput(template, scope) { evalOutput: function(template, scope) {
assert(scope, 'unable to evalOutput: scope undefined'); assert(scope, 'unable to evalOutput: scope undefined');
var val = Exp.evalExp(template.initial, scope); var val = Exp.evalExp(template.initial, scope);
return template.filters return template.filters
.reduce((v, filter) => filter.render(v, scope), val); .reduce((v, filter) => filter.render(v, scope), val);
} },
return { resetRegisters: function(){
renderTemplates, evalOutput, renderTag return this.register = {};
}; }
};
function factory() {
var instance = Object.create(render);
instance.register = {};
return instance;
} }
factory.stringify = stringify; function stringify(val) {
if (typeof val === 'string') return val;
return JSON.stringify(val);
}
module.exports = factory; module.exports = factory;
+2 -2
View File
@@ -12,7 +12,7 @@ module.exports = function(liquid) {
this.variable = match[1]; this.variable = match[1];
this.templates = []; this.templates = [];
var stream = liquid.parseStream(remainTokens); var stream = liquid.parser.parseStream(remainTokens);
stream.onTag('endcapture', token => stream.stop()) stream.onTag('endcapture', token => stream.stop())
.onTemplate(tpl => this.templates.push(tpl)) .onTemplate(tpl => this.templates.push(tpl))
.onEnd(x => { .onEnd(x => {
@@ -21,7 +21,7 @@ module.exports = function(liquid) {
stream.start(); stream.start();
}, },
render: function(scope, hash) { render: function(scope, hash) {
var html = liquid.renderTemplates(this.templates, scope); var html = liquid.renderer.renderTemplates(this.templates, scope);
scope.set(this.variable, html); scope.set(this.variable, html);
} }
}); });
+3 -3
View File
@@ -10,7 +10,7 @@ module.exports = function(liquid) {
this.elseTemplates = []; this.elseTemplates = [];
var p = [], var p = [],
stream = liquid.parseStream(remainTokens) stream = liquid.parser.parseStream(remainTokens)
.onTag('when', token => { .onTag('when', token => {
if (!this.cases[token.args]) { if (!this.cases[token.args]) {
this.cases.push({ this.cases.push({
@@ -35,10 +35,10 @@ module.exports = function(liquid) {
var val = Liquid.evalExp(branch.val, scope); var val = Liquid.evalExp(branch.val, scope);
var cond = Liquid.evalExp(this.cond, scope); var cond = Liquid.evalExp(this.cond, scope);
if (val === cond) { if (val === cond) {
return liquid.renderTemplates(branch.templates, scope); return liquid.renderer.renderTemplates(branch.templates, scope);
} }
} }
return liquid.renderTemplates(this.elseTemplates, scope); return liquid.renderer.renderTemplates(this.elseTemplates, scope);
} }
}); });
+3 -3
View File
@@ -18,7 +18,7 @@ module.exports = function(liquid) {
this.templates = []; this.templates = [];
this.elseTemplates = []; this.elseTemplates = [];
var p, stream = liquid.parseStream(remainTokens) var p, stream = liquid.parser.parseStream(remainTokens)
.onStart(x => p = this.templates) .onStart(x => p = this.templates)
.onTag('else', token => p = this.elseTemplates) .onTag('else', token => p = this.elseTemplates)
.onTag('endfor', token => stream.stop()) .onTag('endfor', token => stream.stop())
@@ -33,7 +33,7 @@ module.exports = function(liquid) {
render: function(scope, hash) { render: function(scope, hash) {
var collection = Liquid.evalExp(this.collection, scope); var collection = Liquid.evalExp(this.collection, scope);
if (Liquid.isFalsy(collection)) { if (Liquid.isFalsy(collection)) {
return liquid.renderTemplates(this.elseTemplates, scope); return liquid.renderer.renderTemplates(this.elseTemplates, scope);
} }
var html = '', var html = '',
@@ -58,7 +58,7 @@ module.exports = function(liquid) {
skip: false skip: false
}; };
scope.push(ctx); scope.push(ctx);
html += liquid.renderTemplates(this.templates, scope); html += liquid.renderer.renderTemplates(this.templates, scope);
var breakloop = scope.get('forloop.stop'); var breakloop = scope.get('forloop.stop');
scope.pop(ctx); scope.pop(ctx);
+3 -3
View File
@@ -9,7 +9,7 @@ module.exports = function(liquid) {
this.branches = []; this.branches = [];
this.elseTemplates = []; this.elseTemplates = [];
var p, stream = liquid.parseStream(remainTokens) var p, stream = liquid.parser.parseStream(remainTokens)
.onStart(x => this.branches.push({ .onStart(x => this.branches.push({
cond: tagToken.args, cond: tagToken.args,
templates: p = [] templates: p = []
@@ -37,10 +37,10 @@ module.exports = function(liquid) {
var branch = this.branches[i]; var branch = this.branches[i];
var cond = Liquid.evalExp(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.renderer.renderTemplates(branch.templates, scope);
} }
} }
return liquid.renderTemplates(this.elseTemplates, scope); return liquid.renderer.renderTemplates(this.elseTemplates, scope);
} }
}); });
+2 -2
View File
@@ -15,7 +15,7 @@ module.exports = function(liquid) {
this.templates = []; this.templates = [];
var p, stream = liquid.parseStream(remainTokens) var p, stream = liquid.parser.parseStream(remainTokens)
.onStart(x => p = this.templates) .onStart(x => p = this.templates)
.onTag('endtablerow', token => stream.stop()) .onTag('endtablerow', token => stream.stop())
.onTemplate(tpl => p.push(tpl)) .onTemplate(tpl => p.push(tpl))
@@ -51,7 +51,7 @@ module.exports = function(liquid) {
ctx[this.variable] = item; ctx[this.variable] = item;
scope.push(ctx); scope.push(ctx);
html += `<td class="col${col}">`; html += `<td class="col${col}">`;
html += liquid.renderTemplates(this.templates, scope); html += liquid.renderer.renderTemplates(this.templates, scope);
html += '</td>'; html += '</td>';
scope.pop(ctx); scope.pop(ctx);
}); });
+3 -3
View File
@@ -4,7 +4,7 @@ var lexical = Liquid.lexical;
module.exports = function(liquid) { module.exports = function(liquid) {
liquid.registerTag('unless', { liquid.registerTag('unless', {
parse: function(tagToken, remainTokens) { parse: function(tagToken, remainTokens) {
var p, stream = liquid.parseStream(remainTokens) var p, stream = liquid.parser.parseStream(remainTokens)
.onStart(x => { .onStart(x => {
p = this.templates = []; p = this.templates = [];
this.cond = tagToken.args; this.cond = tagToken.args;
@@ -22,8 +22,8 @@ module.exports = function(liquid) {
render: function(scope, hash) { render: function(scope, hash) {
var cond = Liquid.evalExp(this.cond, scope); var cond = Liquid.evalExp(this.cond, scope);
return Liquid.isFalsy(cond) ? return Liquid.isFalsy(cond) ?
liquid.renderTemplates(this.templates, scope) : liquid.renderer.renderTemplates(this.templates, scope) :
liquid.renderTemplates(this.elseTemplates, scope); liquid.renderer.renderTemplates(this.elseTemplates, scope);
} }
}); });
}; };
+6 -6
View File
@@ -4,7 +4,7 @@ var sinon = require("sinon");
var expect = chai.expect; var expect = chai.expect;
chai.use(sinonChai); chai.use(sinonChai);
var liquid = require('..')(), ctx; var engine = require('..')(), ctx;
function test(func, cb){ function test(func, cb){
try{ try{
@@ -20,7 +20,7 @@ describe('error', function() {
it('should throw TokenizationError when tag illegal', function() { it('should throw TokenizationError when tag illegal', function() {
test(function(){ test(function(){
liquid.render('{% -a %}', {}); engine.parseAndRender('{% -a %}', {});
}, function(err){ }, function(err){
expect(err.name).to.equal('TokenizationError'); expect(err.name).to.equal('TokenizationError');
expect(err.message).to.equal('illegal tag: {% -a %}'); expect(err.message).to.equal('illegal tag: {% -a %}');
@@ -31,7 +31,7 @@ describe('error', function() {
it('should throw correct error info', function() { it('should throw correct error info', function() {
test(function(){ test(function(){
liquid.render('{%if true%}\naaa{%endif%}\n{% -a %}\n3', {}); engine.parseAndRender('{%if true%}\naaa{%endif%}\n{% -a %}\n3', {});
}, function(err){ }, function(err){
expect(err.input).to.equal('{% -a %}'); expect(err.input).to.equal('{% -a %}');
expect(err.line).to.equal(3); expect(err.line).to.equal(3);
@@ -40,7 +40,7 @@ describe('error', function() {
it('should throw ParseError when filter not exist', function() { it('should throw ParseError when filter not exist', function() {
test(function(){ test(function(){
liquid.render('{{ a | xz }}', {}); engine.parseAndRender('{{ a | xz }}', {});
}, function(err){ }, function(err){
expect(err.name).to.equal('ParseError'); expect(err.name).to.equal('ParseError');
expect(err.message).to.equal('filter "xz" not found'); expect(err.message).to.equal('filter "xz" not found');
@@ -50,7 +50,7 @@ describe('error', function() {
}); });
it('should throw ParseError when tag not exist', function() { it('should throw ParseError when tag not exist', function() {
test(function(){ test(function(){
liquid.render('{% a %}', {}); engine.parseAndRender('{% a %}', {});
}, function(err){ }, function(err){
expect(err.name).to.equal('ParseError'); expect(err.name).to.equal('ParseError');
expect(err.message).to.equal('tag a not found'); expect(err.message).to.equal('tag a not found');
@@ -61,7 +61,7 @@ describe('error', function() {
it('should throw ParseError when tag not closed', function() { it('should throw ParseError when tag not closed', function() {
test(function(){ test(function(){
liquid.render('{% if %}', {}); engine.parseAndRender('{% if %}', {});
}, function(err){ }, function(err){
expect(err.name).to.equal('ParseError'); expect(err.name).to.equal('ParseError');
expect(err.message).to.equal('tag {% if %} not closed'); expect(err.message).to.equal('tag {% if %} not closed');
+1 -1
View File
@@ -18,7 +18,7 @@ function test(src, dst) {
category: 'bar' category: 'bar'
}] }]
}; };
expect(liquid.render(src, ctx)).to.equal(dst); expect(liquid.parseAndRender(src, ctx)).to.equal(dst);
} }
describe('filters', function() { describe('filters', function() {
+26 -10
View File
@@ -1,10 +1,12 @@
const chai = require("chai"); const chai = require("chai");
const expect = chai.expect; const expect = chai.expect;
const should = chai.should;
const Liquid = require('..');
var liquid = require('..')(), describe('liquid', function() {
ctx; var engine, ctx;
beforeEach(function() {
function test(src, dst) { engine = Liquid();
ctx = { ctx = {
date: new Date(), date: new Date(),
foo: 'bar', foo: 'bar',
@@ -18,14 +20,28 @@ function test(src, dst) {
category: 'bar' category: 'bar'
}] }]
}; };
expect(liquid.render(src, ctx)).to.equal(dst); });
}
describe('liquid', function() {
it('should output object', function() { it('should output object', function() {
test('{{obj}}', '{"foo":"bar"}'); engine.parseAndRender('{{obj}}', ctx).should.equal('{"foo":"bar"}');
}); });
it('should output array', function() { it('should output array', function() {
test('{{arr}}', '[-2,"a"]'); engine.parseAndRender('{{arr}}', ctx).should.equal('[-2,"a"]');
});
it('should parse html', function() {
(function(){
engine.parse('{{obj}}');
}).should.not.throw();
(function(){
engine.parse('<html><head>{{obj}}</head></html>');
}).should.not.throw();
});
it('should render template multiple times', function() {
var template = engine.parse('{{obj}}');
engine.render(template, ctx).should.equal('{"foo":"bar"}');
engine.render(template, ctx).should.equal('{"foo":"bar"}');
});
it('should render filters', function() {
var template = engine.parse('<p>{{arr | join: "_"}}</p>');
engine.render(template, ctx).should.equal('<p>-2_a</p>');
}); });
}); });
+1 -1
View File
@@ -8,7 +8,7 @@ chai.use(sinonChai);
var filter = require('../filter.js')(); var filter = require('../filter.js')();
var tag = require('../tag.js')(); var tag = require('../tag.js')();
var Template = require('../template.js'); var Template = require('../parser.js');
describe('template', function() { describe('template', function() {
var scope, template, add = (l, r) => l + r; var scope, template, add = (l, r) => l + r;
+2 -12
View File
@@ -9,13 +9,12 @@ var tag = require('../tag.js')();
var Scope = require('../scope.js'); var Scope = require('../scope.js');
var filter = require('../filter')(); var filter = require('../filter')();
var Render = require('../render.js'); var Render = require('../render.js');
var Template = require('../template.js')(tag, filter); var Template = require('../parser.js')(tag, filter);
describe('render', function() { describe('render', function() {
var scope, render; var scope, render;
beforeEach(function() { beforeEach(function() {
render = Render(filter, tag);
scope = Scope.factory({ scope = Scope.factory({
foo: { foo: {
bar: ['a', 2] bar: ['a', 2]
@@ -23,16 +22,7 @@ describe('render', function() {
}); });
filter.clear(); filter.clear();
tag.clear(); tag.clear();
}); render = Render();
it('should stringify object', function() {
expect(Render.stringify({
foo: 'bar'
})).to.equal('{"foo":"bar"}');
});
it('should stringify object', function() {
expect(Render.stringify([1, 2, 3])).to.equal('[1,2,3]');
}); });
it('should render html', function() { it('should render html', function() {
+2 -2
View File
@@ -5,12 +5,12 @@ var liquid = require('..')(),
ctx, src, dst; ctx, src, dst;
function test(src, dst) { function test(src, dst) {
expect(liquid.render(src, ctx)).to.equal(dst); expect(liquid.parseAndRender(src, ctx)).to.equal(dst);
} }
function testThrow(src, pattern) { function testThrow(src, pattern) {
expect(function() { expect(function() {
liquid.render(src, ctx); liquid.parseAndRender(src, ctx);
}).to.throw(pattern); }).to.throw(pattern);
} }