bogart-edge
Version:
Fast JSGI web framework taking inspiration from Sinatra
306 lines (250 loc) • 8.36 kB
JavaScript
var
path = require('path'),
http = require('http'),
fs = require('fs'),
extname = path.extname,
engines = {},
settings = {},
_ = require('underscore'),
events = require('events'),
q = require('./q'),
when = q.when,
util = require('util'),
readFile = q.promisify(fs.readFile, fs);
var ViewEngine = exports.ViewEngine = function() {
events.EventEmitter.call(this);
};
util.inherits(ViewEngine, events.EventEmitter);
/**
* @param {String} engineName name of the engine 'mustache', 'haml'
* @param {String} views path to the views
* @returns {ViewEngine}
*/
exports.viewEngine = function(engineName, views, opts) {
return Object.create(new ViewEngine(), {
engineName: { value: engineName, enumerable: true, readonly: true },
views: { value: views || path.join(setting('template directory')), enumerable: true, readonly: true },
viewCache: { value: {} },
opts: { value: opts || { cache: false } }
});
};
ViewEngine.prototype.clearCache = function() {
this.viewCache = {};
};
/**
* @api private
*/
ViewEngine.prototype.cacheView = function(view) {
return when(this.read(view), function(str) {
if (this.opts.cache) {
this.cache(view, str);
}
return str;
}.bind(this));
};
ViewEngine.prototype.read = function(view) {
return readFile(isAbsolute(view) ? view : this.viewPath(view), 'utf8');
};
ViewEngine.prototype.cache = function(view, str) {
if (str === undefined) {
return this.viewCache[view];
}
this.viewCache[view] = str;
};
ViewEngine.prototype.viewPath = function(view) {
return path.join(this.views, view);
}
/**
* @api private
*/
ViewEngine.prototype.getCacher = function(view) {
return this.cache.bind(this, view);
}
ViewEngine.prototype.respond = function(view, opts) {
var viewEngine = this;
opts = opts || {};
return q.when(viewEngine.render(view, opts), function(str) {
var headers = _.extend({}, {
'content-type': 'text/html',
'content-length': Buffer.byteLength(str, 'utf-8')
}, opts.headers);
return {
status: opts.status || 200,
headers: headers,
body: [str]
};
});
};
ViewEngine.prototype.render = function(view, opts) {
opts = opts || {};
opts.locals = opts.locals || {};
var
self = this,
ext = extname(view),
engine = this.engineName,
renderer = engine ? exports.viewEngine.engine(engine) : exports.viewEngine.engine(ext.substring(1)),
layout = opts.layout === undefined ? true : opts.layout;
layout = layout === true ? 'layout' + ext : layout;
return when(this.cache(view) || this.cacheView(view),
function success(template) {
var renderedView
, cacher
, jsName = setting('shared js name') || 'javascript'
, namespace = setting('shared js namespace') || 'bogart';
if (renderer === undefined || renderer === null) {
throw new Error('Rendering engine not found for '+engine+'. Try `npm install bogart-'+engine+'`.');
}
if (opts._renderedShared !== true) {
if (opts.locals[jsName]) {
throw 'The locals property of `render` method options contains a property named "'+jsName+'" already. ' +
'The view engine settings specify this name for the shared JavaScript. Please either change the "shared js name" settings ' +
'by executing `var view = require("view"); view.setting("shared js name", "myNewName") or remove the code that is setting ' +
'this property on the opts.locals.';
}
opts.locals[jsName] = '<script type="text/javascript">\n'+namespace+'={};\n';
for (var k in self._shared) {
opts.locals[jsName] += namespace+'["'+k+'"]='+self._shared[k].join('\n');
opts.locals[jsName] += '\n';
}
opts.locals[jsName] += '</script>';
opts._renderedShared = true;
}
self.emit('beforeRender', self, opts, template, cacher);
cacher = self.getCacher(view);
renderedView = renderer(template, opts, cacher, self);
self.emit('afterRender', self, opts, template, cacher);
return when(renderedView, function(renderedView) {
if (layout) {
opts.locals.body = renderedView;
opts.layout = false;
return self.render(layout, opts);
}
return renderedView;
});
});
};
ViewEngine.prototype.partial = function(view, opts) {
opts = opts || {};
opts.locals = opts.locals || {};
opts.partial = true;
opts.layout = false;
return this.render(view, opts);
};
/**
* Share JavaScript with the client side.
*/
ViewEngine.prototype.share = function(obj, name) {
if (obj === undefined || obj === null) {
throw 'Have to share something! First parameter to `ViewEngine.prototype.share` cannot be empty.';
}
this._shared = this._shared || {};
this._shared[name] = this._shared[name] || [];
if (typeof obj === 'string') {
this._shared[name].push(obj);
} else {
this.share(stringify(obj), name);
}
return this;
};
/**
* Inspect JavaScript that is shared with the client side.
*
* @params {String} name The key of the JavaScript to look up.
* @returns {String} JavaScript shared with the client for the `name` specified.
*/
ViewEngine.prototype.shared = function(name) {
this._shared = this._shared || {};
return this._shared[name].join('\n');
};
/**
* Add a template engine
*
* @param {String} engineName The name of the template engine
* @param {Function} render Render a template given the templaet as a string and and options object
*
* @returns undefined
*/
exports.viewEngine.addEngine = function(engineName, render) {
engines[engineName] = render;
};
/**
* Remove a template engine
*
* @param {String} engineName The name of the template engine
* @returns undefined
*/
exports.viewEngine.removeEngine = function(engineName) {
delete engines[engineName];
};
/**
* Retrieves the render function for a registered template engine
*
* @param {String} engineName The name of the template engine
* @returns {Function} Function registered to render templates of the type specified by engineName
*/
exports.viewEngine.engine = function(engineName) {
return engines[engineName];
};
function loadMustachePartials(viewEngine, partials) {
return Object.keys(partials).map(function (partialName) {
var content = viewEngine.cache(partials[partialName]) ||
viewEngine.cacheView(partials[partialName]);
return q.when(content)
.then(function (content) {
return { name: partialName, content: content };
});
});
}
exports.viewEngine.addEngine('mustache', function(str, opts, cache, viewEngine) {
var mustache = require('mustache');
opts = _.extend({}, opts);
if (opts.partials) {
return q.all(loadMustachePartials(viewEngine, opts.partials))
.then(function (partialDescriptors) {
var partials = {};
partialDescriptors.forEach(function (partialDescriptor) {
partials[partialDescriptor.name] = partialDescriptor.content;
});
return partials;
})
.then(function (partials) {
delete opts.partials;
return mustache.to_html(str, opts.locals, partials);
});
} else {
return mustache.to_html(str, opts.locals);
}
});
var setting = exports.setting = function(key, val) {
if (val !== undefined) {
settings[key] = val;
return this;
}
return settings[key];
};
/**
* Returns whether a path is an absolute path.
*/
function isAbsolute(path) {
var windowsDriveRe = /^[a-zA-Z]:/;
return path[0] === '/' || windowsDriveRe.test(path);
}
/**
* Returns a string representation of an object.
* This method is used for sharing objects with the client.
*
* @param {Object} obj An object.
* @returns {String} a string representation of the object.
* @api private
*/
function stringify(obj) {
if (Array.isArray(obj)) {
return '['+obj.map(stringify).join(',')+']';
} else if (obj instanceof Date) {
return 'new Date("'+obj.toString()+'")';
} else if (typeof obj === 'function') {
return obj.toString();
} else {
return JSON.stringify(obj);
}
}