strict_filters/strict_variables when creating engine

This commit is contained in:
harttle
2016-11-07 00:01:02 +08:00
parent 1e7873afc0
commit ddd06e12b5
8 changed files with 203 additions and 90 deletions
+9 -21
View File
@@ -48,7 +48,9 @@ engine.render(tpl, {name: 'alice'})
var engine = Liquid({
root: path.resolve(__dirname, 'views/'), // for layouts and includes
extname: '.liquid',
cache: false
cache: false,
strict_filters: false, // default: false
strict_variables: false // default: false
});
engine.renderFile("hello.liquid", {name: 'alice'})
.then(function(html){
@@ -69,26 +71,11 @@ Defaults to `process.cwd()`
* `cache` indicates whether or not to cache resolved templates. Defaults to `false`.
## Strict Rendering
* `strict_filters` is used to enable strict filter existence. If set to `false`, undefined filters will be rendered as empty string. Otherwise, undefined filters will cause an exception. Defaults to `false`.
Undefined filters and variables will be rendered as empty string by default.
Enable strict rendering to throw errors upon undefined variables/filters:
```javascript
var opts = {
strict_variables: true,
strict_filters: true
};
engine.parseAndRender("{{ foo }}", {}, opts).catch(function(err){
// err.message === undefined variable: foo
});
engine.parseAndRender("{{ 'foo' | filter1 }}", {}, opts).catch(function(err){
// err.message === undefined filter: filter1
});
// Note: the below opts also work:
// engine.render(tpl, ctx, opts)
// engine.renderFile(path, ctx, opts)
```
* `strict_variables` is used to enable strict variable derivation.
If set to `false`, undefined variables will be rendered as empty string.
Otherwise, undefined variables will cause an exception. Defaults to `false`.
## Use with Express.js
@@ -104,7 +91,8 @@ app.set('view engine', 'liquid'); // set to default
> There's an Express demo [here](demo/express/).
Note: includes and layouts lookup path should always be specified by `Liquid({root: []})`.
When using with Express.js, partials(includes and layouts) will be looked up in
both Liquid `root` and Express `views` directories.
## Use in Browser
+2
View File
@@ -38,6 +38,7 @@ var _engine = {
return this.parser.parse(tokens);
},
render: function(tpl, ctx, opts) {
opts = _.assign({}, this.options, opts);
var scope = Scope.factory(ctx, opts);
return this.renderer.renderTemplates(tpl, scope);
},
@@ -73,6 +74,7 @@ var _engine = {
},
lookup: function(filepath, root) {
root = this.options.root.concat(root || []);
root = _.uniq(root);
var paths = root.map(root => path.resolve(root, filepath));
return anySeries(paths, path => statFileAsync(path).then(() => path))
.catch((e) => {
+1 -1
View File
@@ -21,7 +21,7 @@ module.exports = function() {
if (typeof filter !== 'function'){
return {
name: name,
error: new Error(`undefined filter: ${name}`)
error: new TypeError(`undefined filter: ${name}`)
};
}
+3 -10
View File
@@ -4,9 +4,9 @@ const assert = require('./util/assert.js');
const referenceError = /undefined variable|Cannot read property .* of undefined/;
var Scope = {
getAll: function(str) {
getAll: function() {
var ctx = {};
for (i = this.scopes.length - 1; i >= 0; i--) {
for (var i = this.scopes.length - 1; i >= 0; i--) {
_.assign(ctx, this.scopes[i]);
}
return ctx;
@@ -26,13 +26,6 @@ var Scope = {
throw new TypeError('undefined variable: ' + str);
}
},
safeGet: function(str) {
try {
var val = this.safeGet(str);
} catch (e) {;
}
return val;
},
set: function(k, v) {
this.setPropertyByPath(this.scopes[this.scopes.length - 1], k, v);
return this;
@@ -112,7 +105,7 @@ var Scope = {
}
// foo["bar"]
else {
var j = str.indexOf(delemiter, i + 2);
j = str.indexOf(delemiter, i + 2);
assert(j !== -1, `unbalanced ${delemiter}: ${str}`);
name = str.slice(i + 2, j);
seq.push(name);
+51 -3
View File
@@ -25,27 +25,75 @@ function forOwn(object, iteratee) {
return object;
}
function assign(dst, src) {
dst = dst || {};
/*
* Assigns own enumerable string keyed properties of source objects to the destination object.
* Source objects are applied from left to right.
* Subsequent sources overwrite property assignments of previous sources.
*
* Note: This method mutates object and is loosely based on Object.assign.
*
* @param {Object} object The destination object.
* @param {...Object} sources The source objects.
* @return {Object} Returns object.
*/
function assign(object) {
object = isObject(object) ? object : {};
var srcs = Array.prototype.slice.call(arguments, 1);
srcs.forEach(function(src) {
_assignBinary(object, src);
});
return object;
}
function _assignBinary(dst, src) {
if (!dst) return dst;
forOwn(src, function(v, k) {
dst[k] = v;
});
return dst;
}
function isArray(value) {
return value instanceof Array;
}
function echo(prefix){
function echo(prefix) {
return v => {
console.log('[' + prefix + ']', v);
return v;
};
}
function uniq(arr) {
var u = {},
a = [];
for (var i = 0, l = arr.length; i < l; ++i) {
if (u.hasOwnProperty(arr[i])) {
continue;
}
a.push(arr[i]);
u[arr[i]] = 1;
}
return a;
}
/*
* Checks if value is the language type of Object.
* (e.g. arrays, functions, objects, regexes, new Number(0), and new String(''))
* @param {any} value The value to check.
* @return {Boolean} Returns true if value is an object, else false.
*/
function isObject(value) {
return value !== null && typeof value === 'object';
}
exports.isString = isString;
exports.isArray = isArray;
exports.isObject = isObject;
exports.forOwn = forOwn;
exports.assign = assign;
exports.uniq = uniq;
exports.echo = echo;
+4 -5
View File
@@ -1,5 +1,4 @@
const chai = require("chai");
const should = chai.should();
const expect = chai.expect;
const Liquid = require('..');
const mock = require('mock-fs');
@@ -152,10 +151,10 @@ describe('liquid', function() {
it('should be disabled by default', function() {
return engine.renderFile('files/foo')
.then(x => expect(x).to.equal('foo'))
.then(x => mock({
.then(() => mock({
'/root/files/foo.html': 'bar'
}))
.then(x => engine.renderFile('files/foo'))
.then(() => engine.renderFile('files/foo'))
.then(x => expect(x).to.equal('bar'));
});
it('should respect cache=true option', function() {
@@ -166,10 +165,10 @@ describe('liquid', function() {
});
return engine.renderFile('files/foo')
.then(x => expect(x).to.equal('foo'))
.then(x => mock({
.then(() => mock({
'/root/files/foo.html': 'bar'
}))
.then(x => engine.renderFile('files/foo'))
.then(() => engine.renderFile('files/foo'))
.then(x => expect(x).to.equal('foo'));
});
});
+86 -48
View File
@@ -1,65 +1,103 @@
const chai = require("chai");
const expect = chai.expect;
const mock = require('mock-fs');
chai.use(require("chai-as-promised"));
var engine = require('../..')(), ctx;
function test(promise, cb){
return promise
.then((result) => {
return cb({});
})
.catch((error) => {
return cb(error);
});
}
var engine = require('../..')();
var strictEngine = require('../..')({
strict_variables: true,
strict_filters: true
});
describe('error', function() {
it('should throw TokenizationError when tag illegal', function() {
return test(engine.parseAndRender('{% . a %}', {}), function(err){
expect(err.name).to.equal('TokenizationError');
expect(err.message).to.equal('illegal tag: {% . a %}');
expect(err.input).to.equal('{% . a %}');
expect(err.line).to.equal(1);
describe('TokenizationError', function() {
it('should throw TokenizationError when tag illegal', function() {
return engine.parseAndRender('{% . a %}', {}).catch(function(err) {
expect(err.name).to.equal('TokenizationError');
expect(err.message).to.equal('illegal tag: {% . a %}');
expect(err.input).to.equal('{% . a %}');
expect(err.line).to.equal(1);
});
});
it('should throw TokenizationError when tag syntax illegal', function() {
return engine.parseAndRender('{% . a }', {}).catch(function(err) {
expect(err.name).to.equal('TokenizationError');
expect(err.message).to.equal('illegal tag: {% . a }');
expect(err.input).to.equal('{% . a }');
expect(err.line).to.equal(1);
});
});
it('should throw TokenizationError when filter syntax illegal', function() {
return engine.parseAndRender('{{ a|| }}', {}).catch(function(err) {
expect(err.name).to.equal('TokenizationError');
expect(err.message).to.equal('{{ a|| }');
expect(err.input).to.equal('{{ a|| }');
expect(err.line).to.equal(1);
});
});
});
it('should throw correct error info', function() {
return test(engine.parseAndRender('{%if true%}\naaa{%endif%}\n{% -a %}\n3', {}), function(err){
expect(err.input).to.equal('{% -a %}');
expect(err.line).to.equal(3);
describe('TypeError', function() {
it('should not throw when variable undefined by default', function() {
return expect(engine.parseAndRender('X{{a}}Y')).to.eventually.equal('XY');
});
it('should throw TypeError when variable not defined', function() {
return expect(strictEngine.parseAndRender('{{a}}')).to.eventually
.be.rejected
.then(function(e){
expect(e).to.have.property('name', 'TypeError');
expect(e).to.have.property('message', 'undefined variable: a');
});
});
it('should throw TypeError when filter not defined', function() {
return expect(strictEngine.parseAndRender('{{1 | a}}')).to.eventually
.be.rejected
.then(function(e){
expect(e).to.have.property('name', 'TypeError');
expect(e).to.have.property('message', 'undefined filter: a');
});
});
});
it('should throw correct error info for files', function() {
mock({
"/foo.html": '<html>\n<head>\n\n{% raw %}\n\n'
describe('ParseError', function() {
it('should throw correct error info', function() {
var src = '{%if true%}\naaa{%endif%}\n{% -a %}\n3';
return engine.parseAndRender(src).catch(function(err) {
expect(err.name).to.equal('ParseError');
expect(err.input).to.equal('{% -a %}');
expect(err.line).to.equal(3);
});
});
return test(engine.renderFile('/foo.html', {}), function(err){
mock.restore();
expect(err.input).to.equal('{% raw %}');
expect(err.line).to.equal(4);
expect(err.file).to.equal('/foo.html');
it('should throw correct error info for files', function() {
mock({
"/foo.html": '<html>\n<head>\n\n{% raw %}\n\n'
});
return engine.renderFile('/foo.html').catch(function(err) {
mock.restore();
expect(err.name).to.equal('ParseError');
expect(err.input).to.equal('{% raw %}');
expect(err.line).to.equal(4);
expect(err.file).to.equal('/foo.html');
});
});
it('should throw ParseError when tag not exist', function() {
return engine.parseAndRender('{% a %}').catch(function(err) {
expect(err.name).to.equal('ParseError');
expect(err.message).to.equal('tag a not found');
expect(err.input).to.equal('{% a %}');
expect(err.line).to.equal(1);
});
});
it('should throw ParseError when tag not closed', function() {
return engine.parseAndRender('{% if %}').catch(function(err) {
expect(err.name).to.equal('ParseError');
expect(err.message).to.equal('tag {% if %} not closed');
expect(err.input).to.equal('{% if %}');
expect(err.line).to.equal(1);
});
});
});
it('should throw ParseError when tag not exist', function() {
return test(engine.parseAndRender('{% a %}', {}), function(err){
expect(err.name).to.equal('ParseError');
expect(err.message).to.equal('tag a not found');
expect(err.input).to.equal('{% a %}');
expect(err.line).to.equal(1);
});
});
it('should throw ParseError when tag not closed', function() {
return test(engine.parseAndRender('{% if %}', {}), function(err){
expect(err.name).to.equal('ParseError');
expect(err.message).to.equal('tag {% if %} not closed');
expect(err.input).to.equal('{% if %}');
expect(err.line).to.equal(1);
});
});
});
+47 -2
View File
@@ -11,7 +11,7 @@ describe('util/underscore', function() {
expect(_.isString('foo')).to.be.true;
});
it('should return true String instance', function() {
expect(_.isString(new String('foo'))).to.be.true;
expect(_.isString(String('foo'))).to.be.true;
});
it('should return false for 123 ', function() {
expect(_.isString(123)).to.be.false;
@@ -53,7 +53,7 @@ describe('util/underscore', function() {
expect(_.isArray("foo")).to.be.false;
});
});
describe('.echo()', function(){
describe('.echo()', function() {
it('should be transparent', function() {
expect(_.echo('foo')('bar')).to.equal('bar');
});
@@ -63,4 +63,49 @@ describe('util/underscore', function() {
expect(log).to.have.been.calledWith('[foo]', 'bar');
});
});
describe('.assign()', function() {
it('should handle null dst', function() {
expect(_.assign(null, {
foo: 'bar'
})).to.deep.equal({
foo: 'bar'
});
});
it('should assign 2 objects', function() {
var src = {
foo: 'foo',
bar: 'bar'
};
var dst = {
foo: 'bar',
kaa: 'kaa'
};
expect(_.assign(dst, src)).to.deep.equal({
foo: 'foo',
bar: 'bar',
kaa: 'kaa'
});
});
it('should assign 3 objects', function() {
expect(_.assign({
foo: 'foo'
}, {
bar: 'bar'
}, {
car: 'car'
})).to.deep.equal({
foo: 'foo',
bar: 'bar',
car: 'car'
});
});
});
describe('.uniq()', function() {
it('should handle empty array', function() {
expect(_.uniq([])).to.deep.equal([]);
});
it('should do uniq', function() {
expect(_.uniq([1, 'a', 'a', 1])).to.deep.equal([1, 'a']);
});
});
});