diff --git a/README.md b/README.md index ee9b2a76a..48d1bc873 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,58 @@ shall be implemented. > [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: + +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: + ## Operators Documentation: diff --git a/index.js b/index.js index 118e40410..88c58f104 100644 --- a/index.js +++ b/index.js @@ -6,61 +6,62 @@ const path = require("path"); const fs = require('fs'); const Tag = require('./tag.js'); const Filter = require('./filter.js'); -const error = require('./error.js'); -const Template = require('./template'); +const Template = require('./parser'); const Expression = require('./expression.js'); - const tagsPath = path.join(__dirname, "tags"); 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); }, - registerTag : function(name, tag){ + registerTag: function(name, tag) { return this.tag.register(name, tag); - } + }, }; -function factory(){ +function factory() { var engine = Object.create(_engine); - - engine.tag = Tag(); - 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); + engine.init(Tag(), Filter()); + registerTagsAndFilters(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.error = error; factory.isTruthy = Expression.isTruthy; factory.isFalsy = Expression.isFalsy; -factory.stringify = Render.stringify; factory.evalExp = Expression.evalExp; factory.evalValue = Expression.evalValue; - module.exports = factory; diff --git a/template.js b/parser.js similarity index 100% rename from template.js rename to parser.js diff --git a/render.js b/render.js index bfb7f7b6e..a07f1e2af 100644 --- a/render.js +++ b/render.js @@ -2,36 +2,32 @@ const error = require('./error.js'); const Exp = require('./expression.js'); const assert = require('assert'); -function stringify(val) { - if (typeof val === 'string') return val; - return JSON.stringify(val); -} +var render = { -function factory(Filter, Tag) { - - function renderTemplates(templates, scope) { + renderTemplates: function(templates, scope) { assert(scope, 'unable to evalTemplates: scope undefined'); - var html = '', partial; + var html = '', + partial; templates.some(template => { if (scope.get('forloop.skip')) return true; switch (template.type) { case 'tag': - partial = renderTag(template, scope, this.register); - if(partial === undefined) return true; + partial = this.renderTag(template, scope, this.register); + if (partial === undefined) return true; html += partial; break; case 'html': html += template.value; break; case 'output': - var val = evalOutput(template, scope); + var val = this.evalOutput(template, scope); html += stringify(val); } }); return html; - } + }, - function renderTag(template, scope, register) { + renderTag: function(template, scope, register) { if (template.name === 'continue') { scope.set('forloop.skip', true); return; @@ -42,20 +38,29 @@ function factory(Filter, Tag) { return; } return template.render(scope, register); - } + }, - function evalOutput(template, scope) { + evalOutput: function(template, scope) { assert(scope, 'unable to evalOutput: scope undefined'); var val = Exp.evalExp(template.initial, scope); return template.filters .reduce((v, filter) => filter.render(v, scope), val); - } + }, - return { - renderTemplates, evalOutput, renderTag - }; + resetRegisters: function(){ + 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; diff --git a/tags/capture.js b/tags/capture.js index a655b4725..42b11b4e0 100644 --- a/tags/capture.js +++ b/tags/capture.js @@ -12,7 +12,7 @@ module.exports = function(liquid) { this.variable = match[1]; this.templates = []; - var stream = liquid.parseStream(remainTokens); + var stream = liquid.parser.parseStream(remainTokens); stream.onTag('endcapture', token => stream.stop()) .onTemplate(tpl => this.templates.push(tpl)) .onEnd(x => { @@ -21,7 +21,7 @@ module.exports = function(liquid) { stream.start(); }, render: function(scope, hash) { - var html = liquid.renderTemplates(this.templates, scope); + var html = liquid.renderer.renderTemplates(this.templates, scope); scope.set(this.variable, html); } }); diff --git a/tags/case.js b/tags/case.js index c8d0c8d0a..a92fdfbd0 100644 --- a/tags/case.js +++ b/tags/case.js @@ -10,7 +10,7 @@ module.exports = function(liquid) { this.elseTemplates = []; var p = [], - stream = liquid.parseStream(remainTokens) + stream = liquid.parser.parseStream(remainTokens) .onTag('when', token => { if (!this.cases[token.args]) { this.cases.push({ @@ -35,10 +35,10 @@ module.exports = function(liquid) { 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.renderer.renderTemplates(branch.templates, scope); } } - return liquid.renderTemplates(this.elseTemplates, scope); + return liquid.renderer.renderTemplates(this.elseTemplates, scope); } }); diff --git a/tags/for.js b/tags/for.js index 32f6298ce..6a490456f 100644 --- a/tags/for.js +++ b/tags/for.js @@ -18,7 +18,7 @@ module.exports = function(liquid) { this.templates = []; this.elseTemplates = []; - var p, stream = liquid.parseStream(remainTokens) + var p, stream = liquid.parser.parseStream(remainTokens) .onStart(x => p = this.templates) .onTag('else', token => p = this.elseTemplates) .onTag('endfor', token => stream.stop()) @@ -33,7 +33,7 @@ module.exports = function(liquid) { render: function(scope, hash) { var collection = Liquid.evalExp(this.collection, scope); if (Liquid.isFalsy(collection)) { - return liquid.renderTemplates(this.elseTemplates, scope); + return liquid.renderer.renderTemplates(this.elseTemplates, scope); } var html = '', @@ -58,7 +58,7 @@ module.exports = function(liquid) { skip: false }; scope.push(ctx); - html += liquid.renderTemplates(this.templates, scope); + html += liquid.renderer.renderTemplates(this.templates, scope); var breakloop = scope.get('forloop.stop'); scope.pop(ctx); diff --git a/tags/if.js b/tags/if.js index 1dbe85776..9c0663023 100644 --- a/tags/if.js +++ b/tags/if.js @@ -9,7 +9,7 @@ module.exports = function(liquid) { this.branches = []; this.elseTemplates = []; - var p, stream = liquid.parseStream(remainTokens) + var p, stream = liquid.parser.parseStream(remainTokens) .onStart(x => this.branches.push({ cond: tagToken.args, templates: p = [] @@ -37,10 +37,10 @@ module.exports = function(liquid) { var branch = this.branches[i]; var cond = Liquid.evalExp(branch.cond, scope); 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); } }); diff --git a/tags/tablerow.js b/tags/tablerow.js index bae0b6cec..d6932c03f 100644 --- a/tags/tablerow.js +++ b/tags/tablerow.js @@ -15,7 +15,7 @@ module.exports = function(liquid) { this.templates = []; - var p, stream = liquid.parseStream(remainTokens) + var p, stream = liquid.parser.parseStream(remainTokens) .onStart(x => p = this.templates) .onTag('endtablerow', token => stream.stop()) .onTemplate(tpl => p.push(tpl)) @@ -51,7 +51,7 @@ module.exports = function(liquid) { ctx[this.variable] = item; scope.push(ctx); html += ``; - html += liquid.renderTemplates(this.templates, scope); + html += liquid.renderer.renderTemplates(this.templates, scope); html += ''; scope.pop(ctx); }); diff --git a/tags/unless.js b/tags/unless.js index 988bed864..5f0bca6ba 100644 --- a/tags/unless.js +++ b/tags/unless.js @@ -4,7 +4,7 @@ var lexical = Liquid.lexical; module.exports = function(liquid) { liquid.registerTag('unless', { parse: function(tagToken, remainTokens) { - var p, stream = liquid.parseStream(remainTokens) + var p, stream = liquid.parser.parseStream(remainTokens) .onStart(x => { p = this.templates = []; this.cond = tagToken.args; @@ -22,8 +22,8 @@ module.exports = function(liquid) { render: function(scope, hash) { var cond = Liquid.evalExp(this.cond, scope); return Liquid.isFalsy(cond) ? - liquid.renderTemplates(this.templates, scope) : - liquid.renderTemplates(this.elseTemplates, scope); + liquid.renderer.renderTemplates(this.templates, scope) : + liquid.renderer.renderTemplates(this.elseTemplates, scope); } }); }; diff --git a/test/error.js b/test/error.js index 92f7e376a..b1d93e4a7 100644 --- a/test/error.js +++ b/test/error.js @@ -4,7 +4,7 @@ var sinon = require("sinon"); var expect = chai.expect; chai.use(sinonChai); -var liquid = require('..')(), ctx; +var engine = require('..')(), ctx; function test(func, cb){ try{ @@ -20,7 +20,7 @@ describe('error', function() { it('should throw TokenizationError when tag illegal', function() { test(function(){ - liquid.render('{% -a %}', {}); + engine.parseAndRender('{% -a %}', {}); }, function(err){ expect(err.name).to.equal('TokenizationError'); expect(err.message).to.equal('illegal tag: {% -a %}'); @@ -31,7 +31,7 @@ describe('error', function() { it('should throw correct error info', function() { test(function(){ - liquid.render('{%if true%}\naaa{%endif%}\n{% -a %}\n3', {}); + engine.parseAndRender('{%if true%}\naaa{%endif%}\n{% -a %}\n3', {}); }, function(err){ expect(err.input).to.equal('{% -a %}'); expect(err.line).to.equal(3); @@ -40,7 +40,7 @@ describe('error', function() { it('should throw ParseError when filter not exist', function() { test(function(){ - liquid.render('{{ a | xz }}', {}); + engine.parseAndRender('{{ a | xz }}', {}); }, function(err){ expect(err.name).to.equal('ParseError'); 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() { test(function(){ - liquid.render('{% a %}', {}); + engine.parseAndRender('{% a %}', {}); }, function(err){ expect(err.name).to.equal('ParseError'); 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() { test(function(){ - liquid.render('{% if %}', {}); + engine.parseAndRender('{% if %}', {}); }, function(err){ expect(err.name).to.equal('ParseError'); expect(err.message).to.equal('tag {% if %} not closed'); diff --git a/test/filters.js b/test/filters.js index 3ce7f5aaf..7bcb52cfe 100644 --- a/test/filters.js +++ b/test/filters.js @@ -18,7 +18,7 @@ function test(src, dst) { category: 'bar' }] }; - expect(liquid.render(src, ctx)).to.equal(dst); + expect(liquid.parseAndRender(src, ctx)).to.equal(dst); } describe('filters', function() { diff --git a/test/liquid.js b/test/liquid.js index 067035139..ff8458dce 100644 --- a/test/liquid.js +++ b/test/liquid.js @@ -1,31 +1,47 @@ const chai = require("chai"); const expect = chai.expect; - -var liquid = require('..')(), - ctx; - -function test(src, dst) { - ctx = { - date: new Date(), - foo: 'bar', - arr: [-2, 'a'], - obj: { - foo: 'bar' - }, - posts: [{ - category: 'foo' - }, { - category: 'bar' - }] - }; - expect(liquid.render(src, ctx)).to.equal(dst); -} +const should = chai.should; +const Liquid = require('..'); describe('liquid', function() { + var engine, ctx; + beforeEach(function() { + engine = Liquid(); + ctx = { + date: new Date(), + foo: 'bar', + arr: [-2, 'a'], + obj: { + foo: 'bar' + }, + posts: [{ + category: 'foo' + }, { + category: 'bar' + }] + }; + }); it('should output object', function() { - test('{{obj}}', '{"foo":"bar"}'); + engine.parseAndRender('{{obj}}', ctx).should.equal('{"foo":"bar"}'); }); 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('{{obj}}'); + }).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('

{{arr | join: "_"}}

'); + engine.render(template, ctx).should.equal('

-2_a

'); }); }); diff --git a/test/template.js b/test/parser.js similarity index 96% rename from test/template.js rename to test/parser.js index eb9123d58..e4c1d227f 100644 --- a/test/template.js +++ b/test/parser.js @@ -8,7 +8,7 @@ chai.use(sinonChai); var filter = require('../filter.js')(); var tag = require('../tag.js')(); -var Template = require('../template.js'); +var Template = require('../parser.js'); describe('template', function() { var scope, template, add = (l, r) => l + r; diff --git a/test/render.js b/test/render.js index 6639a4ad4..5ce78a4f3 100644 --- a/test/render.js +++ b/test/render.js @@ -9,13 +9,12 @@ var tag = require('../tag.js')(); var Scope = require('../scope.js'); var filter = require('../filter')(); var Render = require('../render.js'); -var Template = require('../template.js')(tag, filter); +var Template = require('../parser.js')(tag, filter); describe('render', function() { var scope, render; beforeEach(function() { - render = Render(filter, tag); scope = Scope.factory({ foo: { bar: ['a', 2] @@ -23,16 +22,7 @@ describe('render', function() { }); filter.clear(); tag.clear(); - }); - - 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]'); + render = Render(); }); it('should render html', function() { diff --git a/test/tags.js b/test/tags.js index 27b7c1f9c..ec0389d88 100644 --- a/test/tags.js +++ b/test/tags.js @@ -5,12 +5,12 @@ var liquid = require('..')(), ctx, 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) { expect(function() { - liquid.render(src, ctx); + liquid.parseAndRender(src, ctx); }).to.throw(pattern); }