Skip to content

Commit

Permalink
Template path settings. Closes hapijs#1336
Browse files Browse the repository at this point in the history
  • Loading branch information
Eran Hammer committed Dec 29, 2013
1 parent 72405df commit 4b31899
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 68 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ infrastructure. The framework supports a powerful plugin architecture for pain-f

For the latest updates and release information follow [@hapijs](https://twitter.com/hapijs) on twitter.

Current version: **2.0.x**
Current version: **2.1.x**

Node version: **0.10** required

Expand Down
3 changes: 2 additions & 1 deletion docs/Reference.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 2.0.x API Reference
# 2.1.x API Reference

- [`Hapi.Server`](#hapiserver)
- [`new Server([host], [port], [options])`](#new-serverhost-port-options)
Expand Down Expand Up @@ -267,6 +267,7 @@ When creating a server instance, the following options configure the server's be
for other view templates in the same engine. If `true`, the layout template name must be 'layout.ext' where 'ext' is the engine's extension.
Otherwise, the provided filename is suffixed with the engine's extension and laoded. Disable `layout` when using Jade as it will handle
including any layout files independently. Defaults to `false`.
- `layoutPath` - the root file path where layout templates are located (relative to `basePath` is present). Defaults to `path`.
- `layoutKeyword` - the key used by the template engine to denote where primary template content should go. Defaults to `'content'`.
- `encoding` - the text encoding used by the templates when reading the files and outputting the result. Defaults to `'utf8'`.
- `isCached` - if set to `false`, templates will not be cached (thus will be read from file on every use). Defaults to `true`.
Expand Down
10 changes: 5 additions & 5 deletions lib/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ exports.server = {
// Optional components

cors: false, // CORS headers on responses and OPTIONS requests (defaults: exports.cors): false -> null, true -> defaults, {} -> override defaults
views: null // Views engine
views: undefined // Views engine
};


Expand Down Expand Up @@ -163,9 +163,9 @@ exports.state = {
// Views

exports.views = {
defaultExtension: '',
path: '',
basePath: '',
// defaultExtension: '',
// path: '',
// basePath: '',
compileOptions: {},
runtimeOptions: {},
layout: false,
Expand All @@ -174,7 +174,7 @@ exports.views = {
isCached: true,
allowAbsolutePaths: false,
allowInsecureAccess: false,
partialsPath: '',
// partialsPath: '',
contentType: 'text/html',
compileMode: 'sync'
};
Expand Down
76 changes: 36 additions & 40 deletions lib/schema.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Load modules

var Joi = require('joi');
var Utils = require('./utils');


// Declare internals
Expand Down Expand Up @@ -32,6 +33,29 @@ internals.cache = Joi.object({
});


internals.viewSchema = function (base) {
var schema = {
path: Joi.string(),
basePath: Joi.string(),
compileOptions: Joi.object(),
runtimeOptions: Joi.object(),
layout: Joi.string().allow(false, true),
layoutKeyword: Joi.string(),
layoutPath: Joi.string(),
encoding: Joi.string(),
isCached: Joi.boolean(),
allowAbsolutePaths: Joi.boolean(),
allowInsecureAccess: Joi.boolean(),
partialsPath: Joi.string(),
helpersPath: Joi.string(),
contentType: Joi.string(),
compileMode: Joi.string().valid('sync', 'async')
};

return Utils.merge(schema, base);
};


internals.serverSchema = {
app: Joi.object().allow(null),
cache: [
Expand Down Expand Up @@ -94,24 +118,9 @@ internals.serverSchema = {
server: Joi.number().allow(null, false, true)
}).allow(false, true),
tls: Joi.object().allow(null),
views: Joi.object({
engines: Joi.object().required(),
defaultExtension: Joi.string(),
path: Joi.string(),
basePath: Joi.string(),
compileOptions: Joi.object(),
runtimeOptions: Joi.object(),
layout: Joi.string().allow(false, true),
layoutKeyword: Joi.string(),
encoding: Joi.string(),
isCached: Joi.boolean(),
allowAbsolutePaths: Joi.boolean(),
allowInsecureAccess: Joi.boolean(),
partialsPath: Joi.string(),
helpersPath: Joi.string(),
contentType: Joi.string(),
compileMode: Joi.string().valid('sync', 'async')
}).allow(null),
views: internals.viewSchema({
engines: Joi.object().required()
}),
maxSockets: Joi.number().allow(null)
};

Expand Down Expand Up @@ -253,29 +262,16 @@ internals.routeConfigSchema = {

exports.view = function (options) {

var error = Joi.validate(options, internals.viewSchema);
return (error ? error.annotated() : null);
};
var schema = internals.viewSchema({
module: [
Joi.object({
compile: Joi.func().required()
}).options({ allowUnknown: true }).required(), Joi.string().required()
]
});


internals.viewSchema = {
module: [Joi.object({
compile: Joi.func().required()
}).options({ allowUnknown: true }).required(), Joi.string().required()],
path: Joi.string().allow(''),
basePath: Joi.string().allow(''),
compileOptions: Joi.object(),
runtimeOptions: Joi.object(),
layout: Joi.string().allow(false, true),
layoutKeyword: Joi.string(),
encoding: Joi.string(),
isCached: Joi.boolean(),
allowAbsolutePaths: Joi.boolean(),
allowInsecureAccess: Joi.boolean(),
partialsPath: Joi.string().allow(''),
helpersPath: Joi.string().allow(''),
contentType: Joi.string(),
compileMode: Joi.string().valid('sync', 'async')
var error = Joi.validate(options, schema);
return (error ? error.annotated() : null);
};


Expand Down
45 changes: 29 additions & 16 deletions lib/views.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,12 @@ internals.Manager.prototype.render = function (filename, context, options, callb

var settings = Utils.applyToDefaults(engine.config, options);

this._compile(filename + (fileExtension ? '' : engine.suffix), engine, settings, function (err, compiled) {
var templatePath = this._path(filename + (fileExtension ? '' : engine.suffix), settings);
if (templatePath.isBoom) {
return callback(templatePath);
}

this._compile(templatePath, engine, settings, function (err, compiled) {

if (err) {
return callback(err);
Expand Down Expand Up @@ -232,8 +237,12 @@ internals.Manager.prototype.render = function (filename, context, options, callb
return callback(Boom.badImplementation('settings.layoutKeyword conflict', { context: context, keyword: settings.layoutKeyword }));
}

var layoutFile = (settings.layout === true ? 'layout' : settings.layout) + engine.suffix;
self._compile(layoutFile, engine, settings, function (err, layout) {
var layoutPath = self._path((settings.layout === true ? 'layout' : settings.layout) + engine.suffix, settings, true);
if (layoutPath.isBoom) {
return callback(layoutPath);
}

self._compile(layoutPath, engine, settings, function (err, layout) {

if (err) {
return callback(err);
Expand All @@ -255,13 +264,7 @@ internals.Manager.prototype.render = function (filename, context, options, callb
};


internals.Manager.prototype._compile = function (template, engine, settings, callback) {

if (engine.cache &&
engine.cache[template]) {

return callback(null, engine.cache[template]);
}
internals.Manager.prototype._path = function (template, settings, isLayout) {

// Validate path

Expand All @@ -271,27 +274,37 @@ internals.Manager.prototype._compile = function (template, engine, settings, cal
if (!settings.allowAbsolutePaths &&
isAbsolutePath) {

return callback(Boom.badImplementation('Absolute paths are not allowed in views'));
return Boom.badImplementation('Absolute paths are not allowed in views');
}

if (!settings.allowInsecureAccess &&
isInsecurePath) {

return callback(Boom.badImplementation('View paths cannot lookup templates outside root path (path includes one or more \'../\')'));
return Boom.badImplementation('View paths cannot lookup templates outside root path (path includes one or more \'../\')');
}

// Resolve path and extension

var fullPath = (isAbsolutePath ? template : Path.join(settings.basePath || '', settings.path || '', template));
return (isAbsolutePath ? template : Path.join(settings.basePath || '', (isLayout && settings.layoutPath) || settings.path || '', template));
};


internals.Manager.prototype._compile = function (template, engine, settings, callback) {

if (engine.cache &&
engine.cache[template]) {

return callback(null, engine.cache[template]);
}

settings.compileOptions.filename = fullPath; // Pass the template to Jade via this copy of compileOptions
settings.compileOptions.filename = template; // Pass the template to Jade via this copy of compileOptions

// Read file

Fs.readFile(fullPath, { encoding: settings.encoding }, function (err, data) {
Fs.readFile(template, { encoding: settings.encoding }, function (err, data) {

if (err) {
return callback(Boom.badImplementation('View file not found: ' + fullPath));
return callback(Boom.badImplementation('View file not found: ' + template));
}

engine.compileFunc(data, settings.compileOptions, function (err, compiled) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "hapi",
"description": "HTTP Server framework",
"homepage": "http://hapijs.com",
"version": "2.0.0",
"version": "2.1.0",
"repository": {
"type": "git",
"url": "git://github.com/spumko/hapi"
Expand Down
81 changes: 77 additions & 4 deletions test/integration/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -1751,7 +1751,7 @@ describe('Response', function () {

it('returns response', function (done) {

var layoutServer = new Hapi.Server({ debug: false });
var layoutServer = new Hapi.Server();
layoutServer.views({
engines: { 'html': 'handlebars' },
path: __dirname + '/../unit/templates',
Expand All @@ -1776,7 +1776,7 @@ describe('Response', function () {

it('returns response with layout override', function (done) {

var layoutServer = new Hapi.Server({ debug: false });
var layoutServer = new Hapi.Server();
layoutServer.views({
engines: { 'html': 'handlebars' },
path: __dirname + '/../unit/templates',
Expand All @@ -1801,7 +1801,7 @@ describe('Response', function () {

it('returns response with custom server layout', function (done) {

var layoutServer = new Hapi.Server({ debug: false });
var layoutServer = new Hapi.Server();
layoutServer.views({
engines: { 'html': 'handlebars' },
path: __dirname + '/../unit/templates',
Expand All @@ -1824,9 +1824,82 @@ describe('Response', function () {
});
});

it('returns response without layout', function (done) {
it('returns response with custom server layout and path', function (done) {

var layoutServer = new Hapi.Server();
layoutServer.views({
engines: { 'html': 'handlebars' },
basePath: __dirname + '/../unit',
path: 'templates',
layoutPath: 'templates/layout',
layout: 'elsewhere'
});

var handler = function (request, reply) {

return reply.view('valid/test', { title: 'test', message: 'Hapi' });
};

layoutServer.route({ method: 'GET', path: '/', handler: handler });

layoutServer.inject('/', function (res) {

expect(res.result).to.exist;
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('test+<div>\n <h1>Hapi</h1>\n</div>\n');
done();
});
});

it('errors on missing layout', function (done) {

var layoutServer = new Hapi.Server({ debug: false });
layoutServer.views({
engines: { 'html': 'handlebars' },
path: __dirname + '/../unit/templates',
layout: 'missingLayout'
});

var handler = function (request, reply) {

return reply.view('valid/test', { title: 'test', message: 'Hapi' });
};

layoutServer.route({ method: 'GET', path: '/', handler: handler });

layoutServer.inject('/', function (res) {

expect(res.statusCode).to.equal(500);
done();
});
});

it('errors on invalid layout', function (done) {

var layoutServer = new Hapi.Server({ debug: false });
layoutServer.views({
engines: { 'html': 'handlebars' },
path: __dirname + '/../unit/templates',
layout: '../invalidLayout'
});

var handler = function (request, reply) {

return reply.view('valid/test', { title: 'test', message: 'Hapi' });
};

layoutServer.route({ method: 'GET', path: '/', handler: handler });

layoutServer.inject('/', function (res) {

expect(res.statusCode).to.equal(500);
done();
});
});

it('returns response without layout', function (done) {

var layoutServer = new Hapi.Server();
layoutServer.views({
engines: { 'html': 'handlebars' },
path: __dirname + '/../unit/templates',
Expand Down
1 change: 1 addition & 0 deletions test/unit/templates/layout/elsewhere.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{title}}+{{{ content }}}

0 comments on commit 4b31899

Please sign in to comment.