feature: strict_variables, working on #9

This commit is contained in:
harttle
2016-09-26 22:49:09 +08:00
parent 38916ae864
commit be6c67a1ba
7 changed files with 110 additions and 41 deletions
+18 -9
View File
@@ -1,4 +1,4 @@
const scope = require('./src/scope'); const Scope = require('./src/scope');
const assert = require('assert'); const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
const tokenizer = require('./src/tokenizer.js'); const tokenizer = require('./src/tokenizer.js');
@@ -34,16 +34,22 @@ var _engine = {
var tokens = tokenizer.parse(html); var tokens = tokenizer.parse(html);
return this.parser.parse(tokens); 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(); 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 { try {
var tpl = this.parse(html); var tpl = this.parse(html);
return this.render(tpl, ctx); return this.render(tpl, ctx, opts);
} } catch (error) {
catch (error) {
// A throw inside of a then or catch of a Promise automatically rejects, but since we mix a sync call // 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. // with an async call, we need to do this in case the sync call throws.
return Promise.reject(error); return Promise.reject(error);
@@ -83,8 +89,11 @@ var _engine = {
}); });
}, },
getTemplate: function(filepath) { getTemplate: function(filepath) {
var html = fs.readFileSync(filepath, 'utf8'); return new Promise(function(resolve, reject) {
return Promise.resolve(html); fs.readFile(filepath, 'utf8', function(err, html) {
err ? reject(err) : resolve(html);
});
});
}, },
express: function() { express: function() {
return (filePath, options, callback) => { return (filePath, options, callback) => {
+1 -1
View File
@@ -12,7 +12,7 @@ util.inherits(TokenizationError, Error);
function ParseError(message, input, line) { function ParseError(message, input, line) {
Error.captureStackTrace(this, this.constructor); Error.captureStackTrace(this, this.constructor);
this.name = "ParseError"; this.name = this.constructor.name;
this.message = message || ""; this.message = message || "";
this.input = input; this.input = input;
+8 -3
View File
@@ -2,11 +2,16 @@ const error = require('./error.js');
const Exp = require('./expression.js'); const Exp = require('./expression.js');
const assert = require('assert'); const assert = require('assert');
const Promise = require('any-promise'); const Promise = require('any-promise');
const _ = require('lodash');
var render = { var render = {
renderTemplates: function(templates, scope) { renderTemplates: function(templates, scope, opts) {
assert(scope, 'unable to evalTemplates: scope undefined'); assert(scope, 'unable to evalTemplates: scope undefined');
opts = _.defaults(opts, {
strict_variables: false,
strict_filters: false
});
var html = ''; var html = '';
@@ -15,10 +20,10 @@ var render = {
// emptyPromise.then(renderTag(template0).then(renderTag(template1).then(renderTag(template2)... // emptyPromise.then(renderTag(template0).then(renderTag(template1).then(renderTag(template2)...
var lastPromise = templates.reduce((promise, template) => { var lastPromise = templates.reduce((promise, template) => {
return promise.then((partial) => { return promise.then((partial) => {
if (scope.get('forloop.skip')) { if (scope.safeGet('forloop.skip')) {
return Promise.resolve(''); 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 throw new Error('forloop.stop'); // this will stop/break the sequential promise chain and go to the catch
} }
+26 -11
View File
@@ -1,20 +1,30 @@
const _ = require('lodash'); const _ = require('lodash');
const lexical = require('./lexical.js'); const lexical = require('./lexical.js');
var scope = { var Scope = {
get: function(str) { safeGet: function(str) {
var ctx = {}; var i;
for (var i = this.scopes.length - 1; i >= 0; i--) { // get all
if (str === undefined) { if (str === undefined) {
var ctx = {};
for (i = this.scopes.length - 1; i >= 0; i--) {
_.merge(ctx, this.scopes[i]); _.merge(ctx, this.scopes[i]);
} }
var v = _.get(this.scopes[i], str);
if (v !== undefined) return v;
if(str === undefined){
return ctx; 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;
} }
}, },
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: function(k, v) {
_.set(this.scopes[this.scopes.length - 1], k, v); _.set(this.scopes[this.scopes.length - 1], k, v);
return this; return this;
@@ -28,8 +38,13 @@ var scope = {
} }
}; };
exports.factory = function(_ctx) { exports.factory = function(_ctx, opts) {
var ctx = Object.create(scope); opts = _.defaults(opts, {
ctx.scopes = [_ctx || {}]; strict: false
return ctx; });
var scope = Object.create(Scope);
scope.opts = opts;
scope.scopes = [_ctx || {}];
return scope;
}; };
+2 -1
View File
@@ -13,7 +13,8 @@ describe('expression', function() {
scope = Scope.factory({ scope = Scope.factory({
one: 1, one: 1,
two: 2, two: 2,
x: 'XXX' x: 'XXX',
z: 'z'
}); });
}); });
+35 -14
View File
@@ -74,20 +74,41 @@ describe('liquid', function() {
return engine.renderFile('files/name', ctx).should.eventually.equal('My name is harttle.'); return engine.renderFile('files/name', ctx).should.eventually.equal('My name is harttle.');
}); });
}); });
// todo: make these async describe('#express()', function() {
// describe('#express()', function() { it('should render templates', function() {
// it('should render templates', function() { engine.express()('/root/files/name.html', ctx, function(err, html) {
// engine.express()('/root/files/name.html', ctx, function(err, html) { expect(err).to.equal(null);
// expect(err).to.equal(null); expect(html).to.equal('My name is harttle.');
// expect(html).to.equal('My name is harttle.'); });
// }); });
// }); it('should pass error when file not found', function() {
// it('should pass error when file not found', function() { engine.express()('/root/files/name1.html', ctx, function(err, html) {
// engine.express()('/root/files/name1.html', ctx, function(err, html) { expect(err.code).to.equal('ENOENT');
// 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() { describe('cache', function() {
it('should be disabled by default', function() { it('should be disabled by default', function() {
mock({ mock({
+18
View File
@@ -18,6 +18,24 @@ describe('scope', function() {
scope.get('foo').should.equal('bar'); 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() { it('should get all property', function() {
scope.get().should.deep.equal(ctx); scope.get().should.deep.equal(ctx);
expect(scope.get('')).to.equal(undefined); expect(scope.get('')).to.equal(undefined);