mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-26 13:45:15 -07:00
feature: strict rendering, close #9
This commit is contained in:
@@ -58,6 +58,26 @@ engine.renderFile("hello", {name: 'alice'})
|
|||||||
|
|
||||||
`cache` default to `false`, `extname` default to `.liquid`, `root` default to `""`.
|
`cache` default to `false`, `extname` default to `.liquid`, `root` default to `""`.
|
||||||
|
|
||||||
|
## Strict Rendering
|
||||||
|
|
||||||
|
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:
|
||||||
|
// `engine.render(tpl, ctx, opts)` and `engine.renderFile(path, ctx, opts)` also works.
|
||||||
|
```
|
||||||
|
|
||||||
## Use with Express.js
|
## Use with Express.js
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ var _engine = {
|
|||||||
var scope = Scope.factory(ctx, {
|
var scope = Scope.factory(ctx, {
|
||||||
strict: opts.strict_variables,
|
strict: opts.strict_variables,
|
||||||
});
|
});
|
||||||
return this.renderer.renderTemplates(tpl, scope);
|
return this.renderer.renderTemplates(tpl, scope, opts);
|
||||||
},
|
},
|
||||||
parseAndRender: function(html, ctx, opts) {
|
parseAndRender: function(html, ctx, opts) {
|
||||||
try {
|
try {
|
||||||
@@ -55,10 +55,10 @@ var _engine = {
|
|||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
renderFile: function(filepath, ctx) {
|
renderFile: function(filepath, ctx, opts) {
|
||||||
return this.handleCache(filepath)
|
return this.handleCache(filepath)
|
||||||
.then((templates) => {
|
.then((templates) => {
|
||||||
return this.render(templates, ctx);
|
return this.render(templates, ctx, opts);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
e.file = filepath;
|
e.file = filepath;
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "shopify-liquid",
|
"name": "shopify-liquid",
|
||||||
"version": "1.1.14",
|
"version": "1.1.15",
|
||||||
"description": "Liquid template engine in Node.js (Shopify compliant)",
|
"description": "Liquid template engine in Node.js (Shopify compliant)",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+5
-3
@@ -19,7 +19,10 @@ module.exports = function() {
|
|||||||
|
|
||||||
var name = match[1], argList = match[2] || '', filter = filters[name];
|
var name = match[1], argList = match[2] || '', filter = filters[name];
|
||||||
if (typeof filter !== 'function'){
|
if (typeof filter !== 'function'){
|
||||||
throw new Error(`filter "${name}" not found`);
|
return {
|
||||||
|
name: name,
|
||||||
|
error: new Error(`undefined filter: ${name}`)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var args = [];
|
var args = [];
|
||||||
@@ -37,8 +40,7 @@ module.exports = function() {
|
|||||||
|
|
||||||
function construct(str) {
|
function construct(str) {
|
||||||
var instance = Object.create(_filterInstance);
|
var instance = Object.create(_filterInstance);
|
||||||
instance.parse(str);
|
return instance.parse(str);
|
||||||
return instance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function register(name, filter) {
|
function register(name, filter) {
|
||||||
|
|||||||
+58
-49
@@ -9,7 +9,6 @@ var render = {
|
|||||||
renderTemplates: function(templates, scope, opts) {
|
renderTemplates: function(templates, scope, opts) {
|
||||||
assert(scope, 'unable to evalTemplates: scope undefined');
|
assert(scope, 'unable to evalTemplates: scope undefined');
|
||||||
opts = _.defaults(opts, {
|
opts = _.defaults(opts, {
|
||||||
strict_variables: false,
|
|
||||||
strict_filters: false
|
strict_filters: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -20,52 +19,52 @@ 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.safeGet('forloop.skip')) {
|
if (scope.safeGet('forloop.skip')) {
|
||||||
return Promise.resolve('');
|
return Promise.resolve('');
|
||||||
}
|
}
|
||||||
if (scope.safeGet('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
|
||||||
}
|
}
|
||||||
|
|
||||||
var promiseLink = Promise.resolve('');
|
var promiseLink = Promise.resolve('');
|
||||||
switch (template.type) {
|
switch (template.type) {
|
||||||
case 'tag':
|
case 'tag':
|
||||||
// Add Promises to the chain
|
// Add Promises to the chain
|
||||||
promiseLink = this.renderTag(template, scope, this.register)
|
promiseLink = this.renderTag(template, scope, this.register)
|
||||||
.then((partial) => {
|
.then((partial) => {
|
||||||
if (partial === undefined) {
|
if (partial === undefined) {
|
||||||
return true; // basically a noop (do nothing)
|
return true; // basically a noop (do nothing)
|
||||||
}
|
}
|
||||||
return html += partial;
|
return html += partial;
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'html':
|
case 'html':
|
||||||
promiseLink = Promise.resolve(template.value)
|
promiseLink = Promise.resolve(template.value)
|
||||||
.then((partial) => {
|
.then((partial) => {
|
||||||
return html += partial;
|
return html += partial;
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'output':
|
case 'output':
|
||||||
var val = this.evalOutput(template, scope);
|
var val = this.evalOutput(template, scope, opts);
|
||||||
promiseLink = Promise.resolve(val === undefined ? '' : stringify(val))
|
promiseLink = Promise.resolve(val === undefined ? '' : stringify(val))
|
||||||
.then((partial) => {
|
.then((partial) => {
|
||||||
return html += partial;
|
return html += partial;
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return promiseLink;
|
return promiseLink;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
if (error.message === 'forloop.skip') {
|
if (error.message === 'forloop.skip') {
|
||||||
// the error is a controlled, purposeful stop. so just return the html that we have up to this point
|
// the error is a controlled, purposeful stop. so just return the html that we have up to this point
|
||||||
return html;
|
return html;
|
||||||
} else {
|
} else {
|
||||||
// rethrow actual error
|
// rethrow actual error
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, Promise.resolve('')); // start the reduce chain with a resolved Promise. After first run, the "promise" argument
|
}, Promise.resolve('')); // start the reduce chain with a resolved Promise. After first run, the "promise" argument
|
||||||
// in our reduce callback will be the returned promise from our "then" above. In this
|
// in our reduce callback will be the returned promise from our "then" above. In this
|
||||||
// case, that's the promise returned from this.renderTag or a resolved promise with raw html.
|
// case, that's the promise returned from this.renderTag or a resolved promise with raw html.
|
||||||
|
|
||||||
@@ -92,14 +91,24 @@ var render = {
|
|||||||
return template.render(scope, register);
|
return template.render(scope, register);
|
||||||
},
|
},
|
||||||
|
|
||||||
evalOutput: function(template, scope) {
|
evalOutput: function(template, scope, opts) {
|
||||||
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
|
template.filters.some(filter => {
|
||||||
.reduce((v, filter) => filter.render(v, scope), val);
|
if (filter.error) {
|
||||||
|
if (opts.strict_filters) {
|
||||||
|
throw filter.error;
|
||||||
|
} else { // render as null
|
||||||
|
val = '';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val = filter.render(val, scope);
|
||||||
|
});
|
||||||
|
return val;
|
||||||
},
|
},
|
||||||
|
|
||||||
resetRegisters: function(){
|
resetRegisters: function() {
|
||||||
return this.register = {};
|
return this.register = {};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,14 +42,6 @@ describe('error', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw ParseError when filter not exist', function() {
|
|
||||||
return test(engine.parseAndRender('{{ a | xz }}', {}), function(err){
|
|
||||||
expect(err.name).to.equal('ParseError');
|
|
||||||
expect(err.message).to.equal('filter "xz" not found');
|
|
||||||
expect(err.input).to.equal('{{ a | xz }}');
|
|
||||||
expect(err.line).to.equal(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should throw ParseError when tag not exist', function() {
|
it('should throw ParseError when tag not exist', function() {
|
||||||
return test(engine.parseAndRender('{% a %}', {}), function(err){
|
return test(engine.parseAndRender('{% a %}', {}), function(err){
|
||||||
expect(err.name).to.equal('ParseError');
|
expect(err.name).to.equal('ParseError');
|
||||||
|
|||||||
+4
-4
@@ -14,10 +14,10 @@ describe('filter', function() {
|
|||||||
filter.clear();
|
filter.clear();
|
||||||
scope = Scope.factory();
|
scope = Scope.factory();
|
||||||
});
|
});
|
||||||
it('should throw when not registered', function() {
|
it('should return undefined when not registered', function() {
|
||||||
expect(function() {
|
var result = filter.construct('foo');
|
||||||
filter.construct('foo');
|
expect(result.name).to.equal('foo');
|
||||||
}).to.throw(/filter "foo" not found/);
|
expect(result.error).to.be.an('Error');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse argument syntax', function(){
|
it('should parse argument syntax', function(){
|
||||||
|
|||||||
@@ -37,6 +37,16 @@ describe('liquid', function() {
|
|||||||
it('should output undefined to empty', function() {
|
it('should output undefined to empty', function() {
|
||||||
return engine.parseAndRender('foo{{zzz}}bar', ctx).should.eventually.equal('foobar');
|
return engine.parseAndRender('foo{{zzz}}bar', ctx).should.eventually.equal('foobar');
|
||||||
});
|
});
|
||||||
|
it('should render as null when filter undefined', function() {
|
||||||
|
return engine.parseAndRender('{{arr | filter1}}', ctx).should.eventually.equal('');
|
||||||
|
});
|
||||||
|
it('should throw upon undefined filter when strict_filters set', function() {
|
||||||
|
var opts = {
|
||||||
|
strict_filters: true
|
||||||
|
};
|
||||||
|
return expect(engine.parseAndRender('{{arr | filter1}}', ctx, opts)).to
|
||||||
|
.be.rejectedWith(/undefined filter: filter1/);
|
||||||
|
});
|
||||||
it('should parse html', function() {
|
it('should parse html', function() {
|
||||||
(function() {
|
(function() {
|
||||||
engine.parse('{{obj}}');
|
engine.parse('{{obj}}');
|
||||||
|
|||||||
Reference in New Issue
Block a user