mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-15 04:10:40 -07:00
feature: root option allows array of strings
This commit is contained in:
@@ -45,7 +45,7 @@ engine.render(tpl, {name: 'alice'})
|
||||
|
||||
```javascript
|
||||
var engine = Liquid({
|
||||
root: path.resolve(__dirname, 'views/'), // for layouts and partials
|
||||
root: path.resolve(__dirname, 'views/'), // for layouts and includes
|
||||
extname: '.liquid',
|
||||
cache: false
|
||||
});
|
||||
@@ -60,7 +60,13 @@ engine.renderFile("hello", {name: 'alice'})
|
||||
});
|
||||
```
|
||||
|
||||
`cache` default to `false`, `extname` default to `.liquid`, `root` default to `""`.
|
||||
* `root` is a directory or an array of directories to resolve layouts and includes, as well as the filename passed in when calling `.renderFile()`.
|
||||
If an array, the files are looked up in the order they occur in the array.
|
||||
Defaults to `["."]`
|
||||
|
||||
* `extname` is used to lookup the template file when filepath doesn't include an extension name. Defaults to `.liquid`
|
||||
|
||||
* `cache` indicates whether or not to cache resolved templates. Defaults to `false`.
|
||||
|
||||
## Strict Rendering
|
||||
|
||||
@@ -95,7 +101,9 @@ app.set('views', './views'); // specify the views directory
|
||||
app.set('view engine', 'liquid'); // set to default
|
||||
```
|
||||
|
||||
There's an Express demo [here](demo/express/).
|
||||
> There's an Express demo [here](demo/express/).
|
||||
|
||||
Note: includes and layouts lookup path should always be specified by `Liquid({root: []})`.
|
||||
|
||||
## Use in Browser
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
var Liquid = require('../..');
|
||||
|
||||
var engine = Liquid({
|
||||
root: __dirname, // for layouts and partials
|
||||
extname: '.liquid'
|
||||
});
|
||||
|
||||
app.engine('liquid', engine.express()); // register liquid engine
|
||||
app.set('views', __dirname); // specify the views directory
|
||||
app.set('view engine', 'liquid'); // set to default
|
||||
|
||||
app.get('/', function (req, res) {
|
||||
var todos = ['fork and clone', 'make it better', 'make a pull request'];
|
||||
res.render('todolist', {
|
||||
todos: todos,
|
||||
title: 'Welcome to shopify-liquid!'
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
@@ -1 +0,0 @@
|
||||
<footer>Copyright @ 2016, Harttle</footer>
|
||||
@@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Welcome to shopify-liquid!</h1>
|
||||
{% include 'bar' %}
|
||||
</body>
|
||||
</html>
|
||||
+2
-16
@@ -1,20 +1,6 @@
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
var Liquid = require('..');
|
||||
|
||||
var engine = Liquid({
|
||||
root: __dirname, // for layouts and partials
|
||||
extname: '.liquid'
|
||||
});
|
||||
|
||||
app.engine('liquid', engine.express()); // register liquid engine
|
||||
app.set('views', __dirname); // specify the views directory
|
||||
app.set('view engine', 'liquid'); // set to default
|
||||
|
||||
app.get('/', function (req, res) {
|
||||
res.render('foo');
|
||||
});
|
||||
const app = require('./app.js');
|
||||
|
||||
app.listen(3000, function () {
|
||||
console.log('Example app listening on port 3000!');
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{title}}</h1>
|
||||
|
||||
{% block %}
|
||||
|
||||
<footer> {% block footer %} </footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
{{id}} - {{todo}}
|
||||
@@ -0,0 +1,11 @@
|
||||
{% layout 'layout' %}
|
||||
|
||||
<ul>
|
||||
{% for todo in todolist %}
|
||||
<li>{% include 'todo' id=forloop.index %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% block 'footer' %}
|
||||
Copyright @ 2016, Harttle
|
||||
{% endblock %}
|
||||
@@ -1,6 +1,9 @@
|
||||
const Scope = require('./src/scope');
|
||||
const _ = require('./src/util/underscore.js');
|
||||
const tokenizer = require('./src/tokenizer.js');
|
||||
const fs = require('fs');
|
||||
const statFileAsync = require('./src/util/fs.js').statFileAsync;
|
||||
const readFileAsync = require('./src/util/fs.js').readFileAsync;
|
||||
const pathResolve = require('./src/util/fs.js').pathResolve;
|
||||
const Render = require('./src/render.js');
|
||||
const lexical = require('./src/lexical.js');
|
||||
const Tag = require('./src/tag.js');
|
||||
@@ -10,6 +13,7 @@ const Syntax = require('./src/syntax.js');
|
||||
const tags = require('./tags');
|
||||
const filters = require('./filters');
|
||||
const Promise = require('any-promise');
|
||||
const someSeries = require('./src/util/promise.js').someSeries;
|
||||
|
||||
var _engine = {
|
||||
init: function(tag, filter, options) {
|
||||
@@ -53,7 +57,7 @@ var _engine = {
|
||||
}
|
||||
},
|
||||
renderFile: function(filepath, ctx, opts) {
|
||||
return this.handleCache(filepath)
|
||||
return this.getTemplate(filepath)
|
||||
.then((templates) => {
|
||||
return this.render(templates, ctx, opts);
|
||||
})
|
||||
@@ -72,30 +76,34 @@ var _engine = {
|
||||
registerTag: function(name, tag) {
|
||||
return this.tag.register(name, tag);
|
||||
},
|
||||
handleCache: function(filepath) {
|
||||
if (!filepath) throw new Error('filepath cannot be null');
|
||||
|
||||
return this.getTemplate(filepath)
|
||||
.then((html) => {
|
||||
var tpl = this.options.cache && this.cache[filepath] || this.parse(html);
|
||||
return this.options.cache ? (this.cache[filepath] = tpl) : tpl;
|
||||
});
|
||||
lookup: function(filepath) {
|
||||
var paths = this.options.root.map(root => pathResolve(root, filepath));
|
||||
return someSeries(paths, path => statFileAsync(path).then(() => path));
|
||||
},
|
||||
getTemplate: function(filepath) {
|
||||
filepath = resolvePath(this.options.root, filepath);
|
||||
|
||||
if (!filepath.match(/\.\w+$/)) {
|
||||
filepath += this.options.extname;
|
||||
}
|
||||
return new Promise(function(resolve, reject) {
|
||||
fs.readFile(filepath, 'utf8', function(err, html) {
|
||||
err ? reject(err) : resolve(html);
|
||||
return this
|
||||
.lookup(filepath)
|
||||
.then(filepath => {
|
||||
if (this.options.cache) {
|
||||
var tpl = this.cache[filepath];
|
||||
if (tpl) {
|
||||
return Promise.resolve(tpl);
|
||||
}
|
||||
return readFileAsync(filepath)
|
||||
.then(str => this.parse(str))
|
||||
.then(tpl => this.cache[filepath] = tpl);
|
||||
} else {
|
||||
return readFileAsync(filepath).then(str => this.parse(str));
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
express: function(renderingOptions) {
|
||||
express: function(renderOption) {
|
||||
renderOption = renderOption || {};
|
||||
return (filePath, options, callback) => {
|
||||
this.renderFile(filePath, options, renderingOptions)
|
||||
this.renderFile(filePath, options, renderOption)
|
||||
.then(html => callback(null, html))
|
||||
.catch(e => callback(e));
|
||||
};
|
||||
@@ -104,7 +112,9 @@ var _engine = {
|
||||
|
||||
function factory(options) {
|
||||
options = options || {};
|
||||
options.root = options.root || '';
|
||||
options.root = normalizeStringArray(options.root);
|
||||
if (!options.root.length) options.root = ['.'];
|
||||
|
||||
options.extname = options.extname || '.liquid';
|
||||
|
||||
var engine = Object.create(_engine);
|
||||
@@ -113,17 +123,10 @@ function factory(options) {
|
||||
return engine;
|
||||
}
|
||||
|
||||
function resolvePath(root, path) {
|
||||
if (path[0] == '/') return path;
|
||||
|
||||
var arr = root.split('/').concat(path.split('/'));
|
||||
var result = [];
|
||||
arr.forEach(function(slug) {
|
||||
if (slug == '..') result.pop();
|
||||
else if (!slug || slug == '.');
|
||||
else result.push(slug);
|
||||
});
|
||||
return '/' + result.join('/');
|
||||
function normalizeStringArray(value) {
|
||||
if (_.isArray(value)) return value;
|
||||
if (_.isString(value)) return [value];
|
||||
return [];
|
||||
}
|
||||
|
||||
factory.lexical = lexical;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
const fs = require('fs');
|
||||
|
||||
function readFileAsync(filepath) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
fs.readFile(filepath, 'utf8', function(err, content) {
|
||||
err ? reject(err) : resolve(content);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function statFileAsync(path) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
fs.stat(path, (err, stat) => err ? reject(err) : resolve(stat))
|
||||
});
|
||||
};
|
||||
|
||||
function pathResolve(root, path) {
|
||||
if (path[0] == '/') return path;
|
||||
|
||||
var arr = root.split('/').concat(path.split('/'));
|
||||
var result = [];
|
||||
arr.forEach(function(slug) {
|
||||
if (slug == '..') result.pop();
|
||||
else if (!slug || slug == '.');
|
||||
else result.push(slug);
|
||||
});
|
||||
return '/' + result.join('/');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
readFileAsync,
|
||||
pathResolve,
|
||||
statFileAsync
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
const Promise = require('any-promise');
|
||||
|
||||
/*
|
||||
* Call functions in serial until someone resolved.
|
||||
* @param {Array} iterable the array to iterate with.
|
||||
* @param {Array} iteratee returns a new promise.
|
||||
* The iteratee is invoked with three arguments: (value, index, iterable).
|
||||
*/
|
||||
function someSeries(iterable, iteratee) {
|
||||
var ret = Promise.reject(new Error('init'));
|
||||
iterable.forEach(function(item, idx) {
|
||||
ret = ret
|
||||
.then(x => x)
|
||||
.catch(e => iteratee(item, idx, iterable));
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
exports.someSeries = someSeries;
|
||||
@@ -13,7 +13,7 @@ function isString(value) {
|
||||
* Iteratee functions may exit iteration early by explicitly returning false.
|
||||
* @param {Object} object The object to iterate over.
|
||||
* @param {Function} iteratee The function invoked per iteration.
|
||||
* @return {Object} Returs object.
|
||||
* @return {Object} Returns object.
|
||||
*/
|
||||
function forOwn(object, iteratee) {
|
||||
object = object || {};
|
||||
@@ -25,5 +25,10 @@ function forOwn(object, iteratee) {
|
||||
return object;
|
||||
}
|
||||
|
||||
function isArray(value) {
|
||||
return value instanceof Array;
|
||||
}
|
||||
|
||||
exports.isString = isString;
|
||||
exports.isArray = isArray;
|
||||
exports.forOwn = forOwn;
|
||||
|
||||
+1
-2
@@ -20,7 +20,7 @@ module.exports = function(liquid) {
|
||||
if(this.with){
|
||||
hash[filepath] = Liquid.evalValue(this.with, scope);
|
||||
}
|
||||
return liquid.handleCache(filepath)
|
||||
return liquid.getTemplate(filepath)
|
||||
.then((templates) => {
|
||||
scope.push(hash);
|
||||
return liquid.renderer.renderTemplates(templates, scope);
|
||||
@@ -29,7 +29,6 @@ module.exports = function(liquid) {
|
||||
scope.pop();
|
||||
return html;
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+6
-6
@@ -12,7 +12,7 @@ module.exports = function(liquid) {
|
||||
this.layout = match[0];
|
||||
this.tpls = liquid.parser.parse(remainTokens);
|
||||
},
|
||||
render: function(scope, hash) {
|
||||
render: function(scope) {
|
||||
var layout = Liquid.evalValue(this.layout, scope);
|
||||
|
||||
var html = '';
|
||||
@@ -21,7 +21,7 @@ module.exports = function(liquid) {
|
||||
return liquid.renderer.renderTemplates(this.tpls, scope)
|
||||
.then((partial) => {
|
||||
html += partial;
|
||||
return liquid.handleCache(layout);
|
||||
return liquid.getTemplate(layout);
|
||||
})
|
||||
.then((templates) => {
|
||||
return liquid.renderer.renderTemplates(templates, scope);
|
||||
@@ -43,15 +43,15 @@ module.exports = function(liquid) {
|
||||
this.block = match ? match[0] : 'anonymous';
|
||||
|
||||
this.tpls = [];
|
||||
var p, stream = liquid.parser.parseStream(remainTokens)
|
||||
.on('tag:endblock', token => stream.stop())
|
||||
var stream = liquid.parser.parseStream(remainTokens)
|
||||
.on('tag:endblock', () => stream.stop())
|
||||
.on('template', tpl => this.tpls.push(tpl))
|
||||
.on('end', x => {
|
||||
.on('end', () => {
|
||||
throw new Error(`tag ${token.raw} not closed`);
|
||||
});
|
||||
stream.start();
|
||||
},
|
||||
render: function(scope, hash){
|
||||
render: function(scope){
|
||||
var html = scope.get(`_liquid.blocks.${this.block}`);
|
||||
var promise = Promise.resolve('');
|
||||
if (html === undefined) {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
const chai = require("chai");
|
||||
const expect = chai.expect;
|
||||
const mock = require('mock-fs');
|
||||
const request = require('supertest');
|
||||
const express = require('express');
|
||||
const Liquid = require('..');
|
||||
|
||||
describe('engine#express()', function() {
|
||||
var app, engine;
|
||||
|
||||
before(function() {
|
||||
mock({
|
||||
'/root/foo.html': 'foo',
|
||||
'/views/name.html': 'My name is {{name}}.',
|
||||
'/views/include.html': '{% include file %}',
|
||||
'/partials/bar.html': 'bar'
|
||||
});
|
||||
app = express();
|
||||
engine = Liquid({
|
||||
root: '/root',
|
||||
extname: '.html'
|
||||
});
|
||||
|
||||
app.set('views', ['/views', '/partials']);
|
||||
app.set('view engine', 'html');
|
||||
app.engine('html', engine.express());
|
||||
|
||||
app.get('/name', function(req, res) {
|
||||
res.render('name', {
|
||||
name: 'harttle'
|
||||
});
|
||||
});
|
||||
app.get('/include/:file', function(req, res) {
|
||||
res.render('include', {
|
||||
file: req.params.file
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should render templates', function(done) {
|
||||
request(app).get('/name')
|
||||
.expect('My name is harttle.')
|
||||
.expect(200, done);
|
||||
});
|
||||
it('should pass error when file not found', function(done) {
|
||||
var view = {
|
||||
root: []
|
||||
};
|
||||
var file = '/not-exist.html';
|
||||
var ctx = {};
|
||||
engine.express().call(view, file, ctx, function(err) {
|
||||
expect(err.code).to.equal('ENOENT');
|
||||
console.log(err.message);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('should respect root option when lookup', function(done) {
|
||||
request(app).get('/include/foo')
|
||||
.expect('foo')
|
||||
.expect(200, done);
|
||||
});
|
||||
});
|
||||
+33
-36
@@ -1,10 +1,9 @@
|
||||
const chai = require("chai");
|
||||
const chaiAsPromised = require("chai-as-promised");
|
||||
const should = chai.should();
|
||||
const expect = chai.expect;
|
||||
const Liquid = require('..');
|
||||
const mock = require('mock-fs');
|
||||
chai.use(chaiAsPromised);
|
||||
chai.use(require("chai-as-promised"));
|
||||
|
||||
describe('liquid', function() {
|
||||
var engine, ctx;
|
||||
@@ -28,24 +27,26 @@ describe('liquid', function() {
|
||||
afterEach(function() {
|
||||
mock.restore();
|
||||
});
|
||||
it('should output object', function() {
|
||||
return engine.parseAndRender('{{obj}}', ctx).should.eventually.equal('{"foo":"bar"}');
|
||||
});
|
||||
it('should output array', function() {
|
||||
return engine.parseAndRender('{{arr}}', ctx).should.eventually.equal('[-2,"a"]');
|
||||
});
|
||||
it('should output undefined to empty', function() {
|
||||
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/);
|
||||
describe('{{output}}', function() {
|
||||
it('should output object', function() {
|
||||
return engine.parseAndRender('{{obj}}', ctx).should.eventually.equal('{"foo":"bar"}');
|
||||
});
|
||||
it('should output array', function() {
|
||||
return engine.parseAndRender('{{arr}}', ctx).should.eventually.equal('[-2,"a"]');
|
||||
});
|
||||
it('should output undefined to empty', function() {
|
||||
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() {
|
||||
(function() {
|
||||
@@ -77,10 +78,20 @@ describe('liquid', function() {
|
||||
});
|
||||
describe('#renderFile()', function() {
|
||||
it('should render file', function() {
|
||||
return engine.renderFile('/root/files/foo.html', ctx).should.eventually.equal('foo');
|
||||
return expect(engine.renderFile('/root/files/foo.html', ctx))
|
||||
.to.eventually.equal('foo');
|
||||
});
|
||||
it('should accept relative path', function() {
|
||||
return expect(engine.renderFile('files/foo.html')).to.eventually.equal('foo');
|
||||
return expect(engine.renderFile('files/foo.html'))
|
||||
.to.eventually.equal('foo');
|
||||
});
|
||||
it('should resolve array as root', function(){
|
||||
engine = Liquid({
|
||||
root: ['/boo', '/root/'],
|
||||
extname: '.html'
|
||||
});
|
||||
return expect(engine.renderFile('files/foo.html'))
|
||||
.to.eventually.equal('foo');
|
||||
});
|
||||
it('should render file with context', function() {
|
||||
return engine.renderFile('/root/files/name.html', ctx).should.eventually.equal('My name is harttle.');
|
||||
@@ -102,19 +113,6 @@ describe('liquid', function() {
|
||||
return expect(engine.renderFile('files/foo/../foo.html')).to.eventually.equal('foo');
|
||||
});
|
||||
});
|
||||
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
|
||||
@@ -163,7 +161,6 @@ describe('liquid', function() {
|
||||
.then((result) => {
|
||||
return expect(result).to.equal('foo');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
const chai = require("chai");
|
||||
const sinon = require('sinon');
|
||||
const expect = chai.expect;
|
||||
chai.use(require("chai-as-promised"));
|
||||
chai.use(require("sinon-chai"));
|
||||
|
||||
var P = require('../../src/util/promise.js');
|
||||
|
||||
describe('util/promise', function() {
|
||||
describe('.someSeries()', function() {
|
||||
it('should resolve in series', function() {
|
||||
var spy1 = sinon.spy(),
|
||||
spy2 = sinon.spy();
|
||||
return P
|
||||
.someSeries(
|
||||
['first', 'second'],
|
||||
(item, idx) => new Promise(function(resolve, reject) {
|
||||
if (idx === 0) {
|
||||
setTimeout(function() {
|
||||
spy1();
|
||||
reject(new Error('first cb'));
|
||||
}, 10);
|
||||
} else {
|
||||
spy2();
|
||||
resolve('foo');
|
||||
}
|
||||
}))
|
||||
.then(() => expect(spy2).to.have.been.calledAfter(spy1));
|
||||
});
|
||||
it('should reject when all rejected', function() {
|
||||
var p = P.someSeries(['first', 'second', 'third'],
|
||||
item => Promise.reject(new Error(item)));
|
||||
return expect(p).to.be.rejectedWith("third");
|
||||
});
|
||||
it('should resolve the value that first callback resolved', () => {
|
||||
var p = P.someSeries(['first', 'second'],
|
||||
item => Promise.resolve(item));
|
||||
return expect(p).to.eventually.equal('first');
|
||||
});
|
||||
it('should not call rest of callbacks once resolved', () => {
|
||||
var spy = sinon.spy();
|
||||
return P.someSeries(['first', 'second'], (item, idx) => {
|
||||
if (idx > 0) {
|
||||
spy();
|
||||
}
|
||||
return Promise.resolve(item);
|
||||
})
|
||||
.then(() => expect(spy).to.not.have.been.called);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -45,4 +45,12 @@ describe('util/underscore', function() {
|
||||
expect(spy).to.have.been.calledOnce;
|
||||
});
|
||||
});
|
||||
describe('.isArray()', function() {
|
||||
it('should return true for []', function() {
|
||||
expect(_.isArray([])).to.be.true;
|
||||
});
|
||||
it('should return false for "foo"', function() {
|
||||
expect(_.isArray("foo")).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user