// # Ghost Module // Defines core methods required to build the application // Module dependencies var config = require('./../config'), when = require('when'), express = require('express'), errors = require('./server/errorHandling'), fs = require('fs'), path = require('path'), hbs = require('express-hbs'), nodefn = require('when/node/function'), _ = require('underscore'), Polyglot = require('node-polyglot'), models = require('./server/models'), plugins = require('./server/plugins'), requireTree = require('./server/require-tree'), // Variables appRoot = path.resolve(__dirname, '../'), themePath = path.resolve(appRoot + '/content/themes'), pluginPath = path.resolve(appRoot + '/content/plugins'), themeDirectories = requireTree(themePath), pluginDirectories = requireTree(pluginPath), Ghost, instance, defaults, statuses; // ## Default values /** * A hash of default values to use instead of 'magic' numbers/strings. * @type {Object} */ defaults = { filterPriority: 5, maxPriority: 9 }; // ## Article Statuses /** * A list of atricle status types * @type {Object} */ statuses = { 'draft': 'draft', 'complete': 'complete', 'approved': 'approved', 'scheduled': 'scheduled', 'published': 'published' }; // ## Module Methods /** * @method Ghost * @returns {*} * @constructor */ Ghost = function () { var app, polyglot; if (!instance) { instance = this; // Holds the filters instance.filterCallbacks = []; // Holds the filter hooks (that are built in to Ghost Core) instance.filters = []; // Holds the theme directories temporarily instance.themeDirectories = {}; // Holds the plugin directories temporarily instance.pluginDirectories = {}; // Holds the persistent notifications instance.notifications = []; // Holds the available plugins instance.availablePlugins = {}; app = express(); polyglot = new Polyglot(); _.extend(instance, { app: function () { return app; }, config: function () { return config; }, // there's no management here to be sure this has loaded settings: function () { return instance.settingsCache; }, dataProvider: models, statuses: function () { return statuses; }, polyglot: function () { return polyglot; }, getPaths: function () { return when.all([themeDirectories, pluginDirectories]).then(function (paths) { instance.themeDirectories = paths[0]; instance.pluginDirectories = paths[1]; return; }); }, paths: function () { return { 'appRoot': appRoot, 'themePath': themePath, 'pluginPath': pluginPath, 'activeTheme': path.join(themePath, config.activeTheme), 'adminViews': path.join(appRoot, '/core/server/views/'), 'helperTemplates': path.join(appRoot, '/core/server/helpers/tpl/'), 'lang': path.join(appRoot, '/core/shared/lang/'), 'availableThemes': instance.themeDirectories, 'availablePlugins': instance.pluginDirectories }; } }); } return instance; }; // Initialise the application Ghost.prototype.init = function () { var self = this; return when.join(instance.dataProvider.init(), instance.getPaths()).then(function () { return self.initPlugins(); }, errors.logAndThrowError).then(function () { return self.updateSettingsCache(); }, errors.logAndThrowError); }; // Maintain the internal cache of the settings object Ghost.prototype.updateSettingsCache = function (settings) { var self = this; settings = settings || {}; if (!_.isEmpty(settings)) { self.settingsCache = settings; } else { // TODO: this should use api.browse return models.Settings.findAll().then(function (result) { var settings = {}; _.map(result.models, function (member) { if (!settings.hasOwnProperty(member.attributes.key)) { settings[member.attributes.key] = member.attributes.value; } }); self.settingsCache = settings; }, errors.logAndThrowError); } }; // ## Template utils // Compile a template for a handlebars helper Ghost.prototype.compileTemplate = function (templatePath) { return nodefn.call(fs.readFile, templatePath).then(function (templateContents) { return hbs.handlebars.compile(templateContents.toString()); }, errors.logAndThrowError); }; // Load a template for a handlebars helper Ghost.prototype.loadTemplate = function (name) { var self = this, templateFileName = name + '.hbs', // Check for theme specific version first templatePath = path.join(this.paths().activeTheme, "partials", templateFileName), deferred = when.defer(); // Can't use nodefn here because exists just returns one parameter, true or false fs.exists(templatePath, function (exists) { if (!exists) { // Fall back to helpers templates location templatePath = path.join(self.paths().helperTemplates, templateFileName); } self.compileTemplate(templatePath).then(deferred.resolve, deferred.reject); }); return deferred.promise; }; // Register a handlebars helper for themes Ghost.prototype.registerThemeHelper = function (name, fn) { hbs.registerHelper(name, fn); }; // Register a new filter callback function Ghost.prototype.registerFilter = function (name, priority, fn) { // Curry the priority optional parameter to a default of 5 if (_.isFunction(priority)) { fn = priority; priority = defaults.filterPriority; } this.filterCallbacks[name] = this.filterCallbacks[name] || {}; this.filterCallbacks[name][priority] = this.filterCallbacks[name][priority] || []; this.filterCallbacks[name][priority].push(fn); }; // Unregister a filter callback function Ghost.prototype.unregisterFilter = function (name, priority, fn) { // Curry the priority optional parameter to a default of 5 if (_.isFunction(priority)) { fn = priority; priority = defaults.filterPriority; } // Check if it even exists if (this.filterCallbacks[name] && this.filterCallbacks[name][priority]) { // Remove the function from the list of filter funcs this.filterCallbacks[name][priority] = _.without(this.filterCallbacks[name][priority], fn); } }; // Execute filter functions in priority order Ghost.prototype.doFilter = function (name, args, callback) { var callbacks = this.filterCallbacks[name]; // Bug out early if no callbacks by that name if (!callbacks) { return callback(args); } _.times(defaults.maxPriority + 1, function (priority) { // Bug out if no handlers on this priority if (!_.isArray(callbacks[priority])) { return; } // Call each handler for this priority level _.each(callbacks[priority], function (filterHandler) { try { args = filterHandler(args); } catch (e) { // If a filter causes an error, we log it so that it can be debugged, but do not throw the error errors.logError(e); } }); }); callback(args); }; // Initialise plugins. Will load from config.activePlugins by default Ghost.prototype.initPlugins = function (pluginsToLoad) { pluginsToLoad = pluginsToLoad || config.activePlugins; var self = this; return plugins.init(this, pluginsToLoad).then(function (loadedPlugins) { // Extend the loadedPlugins onto the available plugins _.extend(self.availablePlugins, loadedPlugins); }, errors.logAndThrowError); }; // Initialise Theme or admin Ghost.prototype.initTheme = function (app) { var self = this; return function initTheme(req, res, next) { app.set('view engine', 'hbs'); // return the correct mime type for woff files express['static'].mime.define({'application/font-woff': ['woff']}); if (!res.isAdmin) { app.engine('hbs', hbs.express3( {partialsDir: path.join(self.paths().activeTheme, 'partials')} )); app.set('views', self.paths().activeTheme); } else { app.engine('hbs', hbs.express3({partialsDir: self.paths().adminViews + 'partials'})); app.set('views', self.paths().adminViews); app.use('/public', express['static'](path.join(__dirname, '/client/assets'))); app.use('/public', express['static'](path.join(__dirname, '/client'))); app.use('/shared', express['static'](path.join(__dirname, '/shared/'))); } app.use(express['static'](self.paths().activeTheme)); app.use('/content/images', express['static'](path.join(__dirname, '/../content/images'))); next(); }; }; // TODO: Expose the defaults for other people to see/manipulate as a static value? // Ghost.defaults = defaults; module.exports = Ghost;