diff --git a/README.md b/README.md index c1ae4d658..9042a57d6 100755 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/Reference.md b/docs/Reference.md index ed5bb5420..fe23427ae 100755 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -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) @@ -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`. diff --git a/lib/defaults.js b/lib/defaults.js index 08fa7c02b..b40001aad 100755 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -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 }; @@ -163,9 +163,9 @@ exports.state = { // Views exports.views = { - defaultExtension: '', - path: '', - basePath: '', + // defaultExtension: '', + // path: '', + // basePath: '', compileOptions: {}, runtimeOptions: {}, layout: false, @@ -174,7 +174,7 @@ exports.views = { isCached: true, allowAbsolutePaths: false, allowInsecureAccess: false, - partialsPath: '', + // partialsPath: '', contentType: 'text/html', compileMode: 'sync' }; diff --git a/lib/schema.js b/lib/schema.js index d6302645b..4e52d64fa 100755 --- a/lib/schema.js +++ b/lib/schema.js @@ -1,6 +1,7 @@ // Load modules var Joi = require('joi'); +var Utils = require('./utils'); // Declare internals @@ -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: [ @@ -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) }; @@ -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); }; diff --git a/lib/views.js b/lib/views.js index b6aa7f8ff..81d914595 100755 --- a/lib/views.js +++ b/lib/views.js @@ -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); @@ -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); @@ -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 @@ -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) { diff --git a/package.json b/package.json index 4343d788a..4bd08d1ca 100755 --- a/package.json +++ b/package.json @@ -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" diff --git a/test/integration/response.js b/test/integration/response.js index b8d678012..26eed3e7f 100755 --- a/test/integration/response.js +++ b/test/integration/response.js @@ -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', @@ -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', @@ -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', @@ -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+