diff --git a/index.js b/index.js index e505ba240..9cdf7e9b5 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ -const scope = require('./src/scope'); +const Scope = require('./src/scope'); const assert = require('assert'); const _ = require('lodash'); const tokenizer = require('./src/tokenizer.js'); @@ -34,16 +34,22 @@ var _engine = { var tokens = tokenizer.parse(html); return this.parser.parse(tokens); }, - render: function(tpl, ctx) { + render: function(tpl, ctx, opts) { + opts = _.defaults(opts, { + strict_variables: false, + strict_filters: false + }); this.renderer.resetRegisters(); - return this.renderer.renderTemplates(tpl, scope.factory(ctx)); + var scope = Scope.factory(ctx, { + strict: opts.strict_variables, + }); + return this.renderer.renderTemplates(tpl, scope); }, - parseAndRender: function(html, ctx) { + parseAndRender: function(html, ctx, opts) { try { var tpl = this.parse(html); - return this.render(tpl, ctx); - } - catch (error) { + return this.render(tpl, ctx, opts); + } catch (error) { // A throw inside of a then or catch of a Promise automatically rejects, but since we mix a sync call // with an async call, we need to do this in case the sync call throws. return Promise.reject(error); @@ -83,8 +89,11 @@ var _engine = { }); }, getTemplate: function(filepath) { - var html = fs.readFileSync(filepath, 'utf8'); - return Promise.resolve(html); + return new Promise(function(resolve, reject) { + fs.readFile(filepath, 'utf8', function(err, html) { + err ? reject(err) : resolve(html); + }); + }); }, express: function() { return (filePath, options, callback) => { diff --git a/src/error.js b/src/error.js index 76a62597a..585a85387 100644 --- a/src/error.js +++ b/src/error.js @@ -12,7 +12,7 @@ util.inherits(TokenizationError, Error); function ParseError(message, input, line) { Error.captureStackTrace(this, this.constructor); - this.name = "ParseError"; + this.name = this.constructor.name; this.message = message || ""; this.input = input; diff --git a/src/render.js b/src/render.js index 7812c29fe..1b13f8678 100644 --- a/src/render.js +++ b/src/render.js @@ -2,11 +2,16 @@ const error = require('./error.js'); const Exp = require('./expression.js'); const assert = require('assert'); const Promise = require('any-promise'); +const _ = require('lodash'); var render = { - renderTemplates: function(templates, scope) { + renderTemplates: function(templates, scope, opts) { assert(scope, 'unable to evalTemplates: scope undefined'); + opts = _.defaults(opts, { + strict_variables: false, + strict_filters: false + }); var html = ''; @@ -15,10 +20,10 @@ var render = { // emptyPromise.then(renderTag(template0).then(renderTag(template1).then(renderTag(template2)... var lastPromise = templates.reduce((promise, template) => { return promise.then((partial) => { - if (scope.get('forloop.skip')) { + if (scope.safeGet('forloop.skip')) { return Promise.resolve(''); } - if (scope.get('forloop.stop')) { + if (scope.safeGet('forloop.stop')) { throw new Error('forloop.stop'); // this will stop/break the sequential promise chain and go to the catch } diff --git a/src/scope.js b/src/scope.js index 836b675b3..129f7a128 100644 --- a/src/scope.js +++ b/src/scope.js @@ -1,20 +1,30 @@ const _ = require('lodash'); const lexical = require('./lexical.js'); -var scope = { - get: function(str) { - var ctx = {}; - for (var i = this.scopes.length - 1; i >= 0; i--) { - if(str === undefined){ +var Scope = { + safeGet: function(str) { + var i; + // get all + if (str === undefined) { + var ctx = {}; + for (i = this.scopes.length - 1; i >= 0; i--) { _.merge(ctx, this.scopes[i]); } + return ctx; + } + // get one path + for (i = this.scopes.length - 1; i >= 0; i--) { var v = _.get(this.scopes[i], str); if (v !== undefined) return v; - if(str === undefined){ - return ctx; - } } }, + get: function(str){ + var val = this.safeGet(str); + if (val === undefined && this.opts.strict) { + throw new Error(`[strict_variables] undefined variable: ${str}`); + } + return val; + }, set: function(k, v) { _.set(this.scopes[this.scopes.length - 1], k, v); return this; @@ -28,8 +38,13 @@ var scope = { } }; -exports.factory = function(_ctx) { - var ctx = Object.create(scope); - ctx.scopes = [_ctx || {}]; - return ctx; +exports.factory = function(_ctx, opts) { + opts = _.defaults(opts, { + strict: false + }); + + var scope = Object.create(Scope); + scope.opts = opts; + scope.scopes = [_ctx || {}]; + return scope; }; diff --git a/test/expression.js b/test/expression.js index 436f016c4..ee9265722 100644 --- a/test/expression.js +++ b/test/expression.js @@ -13,7 +13,8 @@ describe('expression', function() { scope = Scope.factory({ one: 1, two: 2, - x: 'XXX' + x: 'XXX', + z: 'z' }); }); diff --git a/test/liquid.js b/test/liquid.js index 2339f1a8a..89d836171 100644 --- a/test/liquid.js +++ b/test/liquid.js @@ -60,7 +60,7 @@ describe('liquid', function() { var template = engine.parse('

{{arr | join: "_"}}

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

-2_a

'); }); - describe('#renderFile()', function(){ + describe('#renderFile()', function() { it('should render file', function() { return engine.renderFile('/root/files/foo.html', ctx).should.eventually.equal('foo'); }); @@ -74,20 +74,41 @@ describe('liquid', function() { return engine.renderFile('files/name', ctx).should.eventually.equal('My name is harttle.'); }); }); - // todo: make these async -// describe('#express()', function() { -// it('should render templates', function() { -// engine.express()('/root/files/name.html', ctx, function(err, html) { -// expect(err).to.equal(null); -// expect(html).to.equal('My name is harttle.'); -// }); -// }); -// it('should pass error when file not found', function() { -// engine.express()('/root/files/name1.html', ctx, function(err, html) { -// expect(err.code).to.equal('ENOENT'); -// }); -// }); -// }); + describe('#express()', function() { + it('should render templates', function() { + engine.express()('/root/files/name.html', ctx, function(err, html) { + expect(err).to.equal(null); + expect(html).to.equal('My name is harttle.'); + }); + }); + it('should pass error when file not found', function() { + engine.express()('/root/files/name1.html', ctx, function(err, html) { + expect(err.code).to.equal('ENOENT'); + }); + }); + }); + describe('strict', function() { + it('should not throw when strict_variables false (default)', function() { + return expect(engine.parseAndRender('before{{notdefined}}after', ctx)).to + .eventually.equal('beforeafter'); + }); + it('should throw when strict_variables true', function() { + var tpl = engine.parse('before{{notdefined}}after'); + var opts = { + strict_variables: true + }; + return expect(engine.render(tpl, ctx, opts)).to + .be.rejectedWith(/undefined variable: notdefined/); + }); + it('should pass strict_variables to render by parseAndRender', function() { + var html = 'before{{notdefined}}after'; + var opts = { + strict_variables: true + }; + return expect(engine.parseAndRender(html, ctx, opts)).to + .be.rejectedWith(/undefined variable: notdefined/); + }); + }); describe('cache', function() { it('should be disabled by default', function() { mock({ diff --git a/test/scope.js b/test/scope.js index 9b5288c05..3fa1ee7b7 100644 --- a/test/scope.js +++ b/test/scope.js @@ -18,6 +18,24 @@ describe('scope', function() { scope.get('foo').should.equal('bar'); }); + it('should get undefined property', function() { + function fn(){ + scope.get('notdefined'); + } + expect(fn).to.not.throw(); + expect(scope.get('notdefined')).to.equal(undefined); + }); + + it('should throw undefined in strict mode', function() { + scope = Scope.factory(ctx, { + strict: true + }); + function fn(){ + scope.get('notdefined'); + } + expect(fn).to.throw(/undefined variable: notdefined/); + }); + it('should get all property', function() { scope.get().should.deep.equal(ctx); expect(scope.get('')).to.equal(undefined);