mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 03:44:29 +03:00
Merge pull request #267 from jgable/pluginApi
Initial Plugin API Implementation
This commit is contained in:
commit
1815656778
@ -41,7 +41,7 @@ config.activeTheme = 'casper';
|
||||
|
||||
|
||||
config.activePlugins = [
|
||||
'fancyFirstChar.js'
|
||||
'FancyFirstChar'
|
||||
];
|
||||
|
||||
// Default Navigation Items
|
||||
|
62
content/plugins/FancyFirstChar/index.js
Normal file
62
content/plugins/FancyFirstChar/index.js
Normal file
@ -0,0 +1,62 @@
|
||||
var util = require('util'),
|
||||
_ = require('underscore'),
|
||||
fancifyPlugin;
|
||||
|
||||
fancifyPlugin = {
|
||||
|
||||
// Fancify a single post body
|
||||
fancify: function (originalContent) {
|
||||
var newContent,
|
||||
firstCharIndex = 0;
|
||||
|
||||
if (originalContent.substr(0, 1) === '<') {
|
||||
firstCharIndex = originalContent.indexOf('>') + 1;
|
||||
}
|
||||
|
||||
newContent = originalContent.substr(0, firstCharIndex);
|
||||
newContent += '<span class="fancyChar">';
|
||||
newContent += originalContent.substr(firstCharIndex, 1);
|
||||
newContent += '</span>';
|
||||
newContent += originalContent.substr(firstCharIndex + 1, originalContent.length - firstCharIndex - 1);
|
||||
|
||||
return newContent;
|
||||
},
|
||||
|
||||
// Fancify a collection of posts
|
||||
fancifyPosts: function (posts) {
|
||||
var self = this;
|
||||
|
||||
if (_.isArray(posts)) {
|
||||
_.each(posts, function (post) {
|
||||
post.content = self.fancify(post.content);
|
||||
});
|
||||
} else if (posts.hasOwnProperty('content')) {
|
||||
posts.content = this.fancify(posts.content);
|
||||
}
|
||||
|
||||
return posts;
|
||||
},
|
||||
|
||||
install: function () {
|
||||
|
||||
},
|
||||
|
||||
uninstall: function () {
|
||||
|
||||
},
|
||||
|
||||
// Registers the prePostsRender filter to alter the content.
|
||||
activate: function (ghost) {
|
||||
ghost.registerFilter('prePostsRender', this.fancifyPosts);
|
||||
},
|
||||
|
||||
// Unregister any filters.
|
||||
deactivate: function (ghost) {
|
||||
ghost.unregisterFilter("prePostsRender", this.fancifyPosts);
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure our this context in the important methods
|
||||
_.bindAll(fancifyPlugin, "fancifyPosts", "fancify", "activate", "deactivate");
|
||||
|
||||
module.exports = fancifyPlugin;
|
@ -1,41 +0,0 @@
|
||||
var _ = require('underscore');
|
||||
|
||||
var fancyFirstChar;
|
||||
|
||||
function fancify(originalContent) {
|
||||
var newContent,
|
||||
firstCharIndex = 0;
|
||||
|
||||
if (originalContent.substr(0, 1) === '<') {
|
||||
firstCharIndex = originalContent.indexOf('>') + 1;
|
||||
}
|
||||
|
||||
newContent = originalContent.substr(0, firstCharIndex);
|
||||
newContent += '<span class="fancyChar">';
|
||||
newContent += originalContent.substr(firstCharIndex, 1);
|
||||
newContent += '</span>';
|
||||
newContent += originalContent.substr(firstCharIndex + 1, originalContent.length - firstCharIndex - 1);
|
||||
|
||||
return newContent;
|
||||
}
|
||||
|
||||
|
||||
fancyFirstChar = {
|
||||
init: function (ghost) {
|
||||
ghost.registerFilter('prePostsRender', function (posts) {
|
||||
if (_.isArray(posts)) {
|
||||
_.each(posts, function (post) {
|
||||
post.content = fancify(post.content);
|
||||
});
|
||||
} else if (posts.hasOwnProperty('content')) {
|
||||
posts.content = fancify(posts.content);
|
||||
}
|
||||
|
||||
return posts;
|
||||
});
|
||||
},
|
||||
activate: function () {},
|
||||
deactivate: function () {}
|
||||
};
|
||||
|
||||
module.exports = fancyFirstChar;
|
@ -15,6 +15,8 @@ var config = require('./../config'),
|
||||
|
||||
models = require('./server/models'),
|
||||
|
||||
plugins = require('./server/plugins'),
|
||||
|
||||
requireTree = require('./server/require-tree'),
|
||||
themePath = path.resolve(__dirname + '../../content/themes'),
|
||||
pluginPath = path.resolve(__dirname + '../../content/plugins'),
|
||||
@ -77,6 +79,8 @@ Ghost = function () {
|
||||
// Holds the persistent notifications
|
||||
instance.notifications = [];
|
||||
|
||||
instance.availablePlugins = {};
|
||||
|
||||
app = express();
|
||||
|
||||
polyglot = new Polyglot();
|
||||
@ -120,36 +124,12 @@ Ghost.prototype.init = function () {
|
||||
var self = this;
|
||||
|
||||
return when.join(instance.dataProvider.init(), instance.getPaths()).then(function () {
|
||||
return self.loadPlugins();
|
||||
return self.initPlugins();
|
||||
}, errors.logAndThrowError).then(function () {
|
||||
return self.updateSettingsCache();
|
||||
}, errors.logAndThrowError);
|
||||
};
|
||||
|
||||
Ghost.prototype.loadPlugins = function () {
|
||||
var self = this,
|
||||
pluginPaths = _.values(self.paths().availablePlugins),
|
||||
pluginPromises = [];
|
||||
|
||||
_.each(self.config().activePlugins, function (plugin) {
|
||||
var match = _.find(pluginPaths, function (path) {
|
||||
return new RegExp(plugin + '$').test(path);
|
||||
});
|
||||
|
||||
if (match) {
|
||||
pluginPromises.push(require(path.join(pluginPath, plugin)));
|
||||
}
|
||||
});
|
||||
|
||||
return when.all(pluginPromises).then(function (plugins) {
|
||||
_.each(plugins, function (plugin) {
|
||||
if (_.isFunction(plugin.init)) {
|
||||
plugin.init(self);
|
||||
}
|
||||
});
|
||||
}, errors.logAndThrowError);
|
||||
};
|
||||
|
||||
Ghost.prototype.updateSettingsCache = function (settings) {
|
||||
var self = this;
|
||||
|
||||
@ -246,6 +226,25 @@ Ghost.prototype.registerFilter = function (name, priority, fn) {
|
||||
this.filterCallbacks[name][priority].push(fn);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {integer} priority
|
||||
* @param {Function} fn
|
||||
*/
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} name [description]
|
||||
* @param {*} args
|
||||
@ -280,10 +279,25 @@ Ghost.prototype.doFilter = function (name, args, callback) {
|
||||
callback(args);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialise plugins. Will load from config.activePlugins by default
|
||||
*
|
||||
* @param {Array} pluginsToLoad
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @todo Tod (?) Old comment
|
||||
* @param {Object} app
|
||||
*/
|
||||
Ghost.prototype.initTheme = function (app) {
|
||||
|
@ -104,6 +104,10 @@ settings = {
|
||||
return dataProvider.Settings.browse(options).then(settingsObject, errors.logAndThrowError);
|
||||
},
|
||||
read: function read(options) {
|
||||
if (_.isString(options)) {
|
||||
options = { key: options };
|
||||
}
|
||||
|
||||
return dataProvider.Settings.read(options.key).then(function (setting) {
|
||||
if (!setting) {
|
||||
return when.reject("Unable to find setting: " + options.key);
|
||||
@ -112,9 +116,26 @@ settings = {
|
||||
return _.pick(setting.toJSON(), 'key', 'value');
|
||||
}, errors.logAndThrowError);
|
||||
},
|
||||
edit: function edit(settings) {
|
||||
settings = settingsCollection(settings);
|
||||
return dataProvider.Settings.edit(settings).then(settingsObject, errors.logAndThrowError);
|
||||
edit: function edit(key, value) {
|
||||
// Check for passing a collection of settings first
|
||||
if (_.isObject(key)) {
|
||||
key = settingsCollection(key);
|
||||
return dataProvider.Settings.edit(key).then(settingsObject, errors.logAndThrowError);
|
||||
}
|
||||
|
||||
return dataProvider.Settings.read(key).then(function (setting) {
|
||||
if (!setting) {
|
||||
return when.reject("Unable to find setting: " + key);
|
||||
}
|
||||
|
||||
if (!_.isString(value)) {
|
||||
value = JSON.stringify(value);
|
||||
}
|
||||
|
||||
setting.set('value', value);
|
||||
|
||||
return dataProvider.Settings.edit(setting);
|
||||
}, errors.logAndThrowError);
|
||||
}
|
||||
};
|
||||
|
||||
|
50
core/server/plugins/GhostPlugin.js
Normal file
50
core/server/plugins/GhostPlugin.js
Normal file
@ -0,0 +1,50 @@
|
||||
|
||||
var GhostPlugin;
|
||||
|
||||
/**
|
||||
* GhostPlugin is the base class for a standard plugin.
|
||||
* @class
|
||||
* @parameter {Ghost} The current Ghost app instance
|
||||
*/
|
||||
GhostPlugin = function (ghost) {
|
||||
this.app = ghost;
|
||||
};
|
||||
|
||||
/**
|
||||
* A method that will be called on installation.
|
||||
* Can optionally return a promise if async.
|
||||
* @parameter {Ghost} The current Ghost app instance
|
||||
*/
|
||||
GhostPlugin.prototype.install = function (ghost) {
|
||||
return;
|
||||
};
|
||||
|
||||
/**
|
||||
* A method that will be called on uninstallation.
|
||||
* Can optionally return a promise if async.
|
||||
* @parameter {Ghost} The current Ghost app instance
|
||||
*/
|
||||
GhostPlugin.prototype.uninstall = function (ghost) {
|
||||
return;
|
||||
};
|
||||
|
||||
/**
|
||||
* A method that will be called when the plugin is enabled.
|
||||
* Can optionally return a promise if async.
|
||||
* @parameter {Ghost} The current Ghost app instance
|
||||
*/
|
||||
GhostPlugin.prototype.activate = function (ghost) {
|
||||
return;
|
||||
};
|
||||
|
||||
/**
|
||||
* A method that will be called when the plugin is disabled.
|
||||
* Can optionally return a promise if async.
|
||||
* @parameter {Ghost} The current Ghost app instance
|
||||
*/
|
||||
GhostPlugin.prototype.deactivate = function (ghost) {
|
||||
return;
|
||||
};
|
||||
|
||||
module.exports = GhostPlugin;
|
||||
|
72
core/server/plugins/index.js
Normal file
72
core/server/plugins/index.js
Normal file
@ -0,0 +1,72 @@
|
||||
|
||||
var _ = require("underscore"),
|
||||
when = require('when'),
|
||||
ghostApi,
|
||||
loader = require("./loader"),
|
||||
GhostPlugin = require("./GhostPlugin");
|
||||
|
||||
function getInstalledPlugins() {
|
||||
if (!ghostApi) {
|
||||
ghostApi = require('../api');
|
||||
}
|
||||
|
||||
return ghostApi.settings.read("installedPlugins").then(function (installed) {
|
||||
installed = installed || "[]";
|
||||
|
||||
try {
|
||||
installed = JSON.parse(installed.value);
|
||||
} catch (e) {
|
||||
return when.reject(e);
|
||||
}
|
||||
|
||||
return installed;
|
||||
});
|
||||
}
|
||||
|
||||
function saveInstalledPlugins(installedPlugins) {
|
||||
return getInstalledPlugins().then(function (currentInstalledPlugins) {
|
||||
var updatedPluginsInstalled = _.uniq(installedPlugins.concat(currentInstalledPlugins));
|
||||
|
||||
return ghostApi.settings.edit("installedPlugins", updatedPluginsInstalled);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
GhostPlugin: GhostPlugin,
|
||||
|
||||
init: function (ghost, pluginsToLoad) {
|
||||
// Grab all installed plugins, install any not already installed that are in pluginsToLoad.
|
||||
return getInstalledPlugins().then(function (installedPlugins) {
|
||||
var loadedPlugins = {},
|
||||
recordLoadedPlugin = function (name, loadedPlugin) {
|
||||
// After loading the plugin, add it to our hash of loaded plugins
|
||||
loadedPlugins[name] = loadedPlugin;
|
||||
|
||||
return when.resolve(loadedPlugin);
|
||||
},
|
||||
loadPromises = _.map(pluginsToLoad, function (plugin) {
|
||||
// If already installed, just activate the plugin
|
||||
if (_.contains(installedPlugins, plugin)) {
|
||||
return loader.activatePluginByName(plugin, ghost).then(function (loadedPlugin) {
|
||||
return recordLoadedPlugin(plugin, loadedPlugin);
|
||||
});
|
||||
}
|
||||
|
||||
// Install, then activate the plugin
|
||||
return loader.installPluginByName(plugin, ghost).then(function () {
|
||||
return loader.activatePluginByName(plugin, ghost);
|
||||
}).then(function (loadedPlugin) {
|
||||
return recordLoadedPlugin(plugin, loadedPlugin);
|
||||
});
|
||||
});
|
||||
|
||||
return when.all(loadPromises).then(function () {
|
||||
// Save our installed plugins to settings
|
||||
return saveInstalledPlugins(_.keys(loadedPlugins));
|
||||
}).then(function () {
|
||||
// Return the hash of all loaded plugins
|
||||
return when.resolve(loadedPlugins);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
81
core/server/plugins/loader.js
Normal file
81
core/server/plugins/loader.js
Normal file
@ -0,0 +1,81 @@
|
||||
|
||||
var path = require("path"),
|
||||
_ = require("underscore"),
|
||||
when = require("when"),
|
||||
ghostInstance,
|
||||
pluginRootDirectory = path.join(process.cwd(), "content/plugins"),
|
||||
loader;
|
||||
|
||||
function getGhostInstance() {
|
||||
if (ghostInstance) {
|
||||
return ghostInstance;
|
||||
}
|
||||
|
||||
var Ghost = require("../../ghost");
|
||||
|
||||
ghostInstance = new Ghost();
|
||||
|
||||
return ghostInstance;
|
||||
}
|
||||
|
||||
function getPluginByName(name, ghost) {
|
||||
ghost = ghost || getGhostInstance();
|
||||
|
||||
// Grab the plugin class to instantiate
|
||||
var PluginClass = require(loader.getPluginRelativePath(name)),
|
||||
plugin;
|
||||
|
||||
// Check for an actual class, otherwise just use whatever was returned
|
||||
if (_.isFunction(PluginClass)) {
|
||||
plugin = new PluginClass(ghost);
|
||||
} else {
|
||||
plugin = PluginClass;
|
||||
}
|
||||
|
||||
return plugin;
|
||||
}
|
||||
|
||||
// The loader is responsible for loading plugins
|
||||
loader = {
|
||||
// Get a relative path to the given plugins root, defaults
|
||||
// to be relative to __dirname
|
||||
getPluginRelativePath: function (name, relativeTo) {
|
||||
relativeTo = relativeTo || __dirname;
|
||||
|
||||
return path.relative(relativeTo, path.join(pluginRootDirectory, name));
|
||||
},
|
||||
|
||||
// Load a plugin and return the instantiated plugin
|
||||
installPluginByName: function (name, ghost) {
|
||||
var plugin = getPluginByName(name, ghost);
|
||||
|
||||
// Check for an install() method on the plugin.
|
||||
if (!_.isFunction(plugin.install)) {
|
||||
return when.reject(new Error("Error loading plugin named " + name + "; no install() method defined."));
|
||||
}
|
||||
|
||||
// Wrapping the install() with a when because it's possible
|
||||
// to not return a promise from it.
|
||||
return when(plugin.install(ghost)).then(function () {
|
||||
return when.resolve(plugin);
|
||||
});
|
||||
},
|
||||
|
||||
// Activate a plugin and return it
|
||||
activatePluginByName: function (name, ghost) {
|
||||
var plugin = getPluginByName(name, ghost);
|
||||
|
||||
// Check for an activate() method on the plugin.
|
||||
if (!_.isFunction(plugin.activate)) {
|
||||
return when.reject(new Error("Error loading plugin named " + name + "; no activate() method defined."));
|
||||
}
|
||||
|
||||
// Wrapping the activate() with a when because it's possible
|
||||
// to not return a promise from it.
|
||||
return when(plugin.activate(ghost)).then(function () {
|
||||
return when.resolve(plugin);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = loader;
|
108
core/test/ghost/plugins_spec.js
Normal file
108
core/test/ghost/plugins_spec.js
Normal file
@ -0,0 +1,108 @@
|
||||
/*globals describe, beforeEach, it*/
|
||||
var _ = require("underscore"),
|
||||
when = require('when'),
|
||||
should = require('should'),
|
||||
sinon = require('sinon'),
|
||||
errors = require('../../server/errorHandling'),
|
||||
helpers = require('./helpers'),
|
||||
plugins = require('../../server/plugins'),
|
||||
GhostPlugin = plugins.GhostPlugin,
|
||||
loader = require('../../server/plugins/loader')
|
||||
|
||||
describe('Plugins', function () {
|
||||
|
||||
before(function (done) {
|
||||
helpers.resetData().then(function () {
|
||||
done();
|
||||
}, done);
|
||||
});
|
||||
|
||||
describe('GhostPlugin Class', function () {
|
||||
|
||||
should.exist(GhostPlugin);
|
||||
|
||||
it('sets app instance', function () {
|
||||
var fakeGhost = {fake: true},
|
||||
plugin = new GhostPlugin(fakeGhost);
|
||||
|
||||
plugin.app.should.equal(fakeGhost);
|
||||
});
|
||||
|
||||
it('has default install, uninstall, activate and deactivate methods', function () {
|
||||
var fakeGhost = {fake: true},
|
||||
plugin = new GhostPlugin(fakeGhost);
|
||||
|
||||
_.isFunction(plugin.install).should.equal(true);
|
||||
_.isFunction(plugin.uninstall).should.equal(true);
|
||||
_.isFunction(plugin.activate).should.equal(true);
|
||||
_.isFunction(plugin.deactivate).should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loader', function () {
|
||||
|
||||
// TODO: These depend heavily on the FancyFirstChar plugin being present.
|
||||
|
||||
it('can load FancyFirstChar by name and unload', function (done) {
|
||||
var fancyPlugin = require("../../../content/plugins/FancyFirstChar"),
|
||||
fakeGhost = {
|
||||
registerFilter: function () { return; },
|
||||
unregisterFilter: function () { return; }
|
||||
},
|
||||
installMock = sinon.stub(fancyPlugin, "install"),
|
||||
uninstallMock = sinon.stub(fancyPlugin, "uninstall"),
|
||||
registerMock = sinon.stub(fakeGhost, "registerFilter"),
|
||||
unregisterMock = sinon.stub(fakeGhost, "unregisterFilter");
|
||||
|
||||
loader.installPluginByName("FancyFirstChar", fakeGhost).then(function (loadedPlugin) {
|
||||
|
||||
should.exist(loadedPlugin);
|
||||
|
||||
installMock.called.should.equal(true);
|
||||
|
||||
loadedPlugin.activate(fakeGhost);
|
||||
|
||||
// Registers the filter
|
||||
registerMock.called.should.equal(true);
|
||||
|
||||
loadedPlugin.deactivate(fakeGhost);
|
||||
|
||||
// Unregisters the filter
|
||||
unregisterMock.called.should.equal(true);
|
||||
|
||||
loadedPlugin.uninstall(fakeGhost);
|
||||
|
||||
done();
|
||||
}, done);
|
||||
});
|
||||
});
|
||||
|
||||
it("can initialize an array of plugins", function (done) {
|
||||
|
||||
var fakeGhost = {
|
||||
registerFilter: function () { return; },
|
||||
unregisterFilter: function () { return; }
|
||||
},
|
||||
installSpy = sinon.spy(loader, "installPluginByName"),
|
||||
activateSpy = sinon.spy(loader, "activatePluginByName");
|
||||
|
||||
plugins.init(fakeGhost, ["FancyFirstChar"]).then(function (loadedPlugins) {
|
||||
should.exist(loadedPlugins);
|
||||
should.exist(loadedPlugins["FancyFirstChar"]);
|
||||
|
||||
installSpy.called.should.equal(true);
|
||||
activateSpy.called.should.equal(true);
|
||||
|
||||
var api = require("../../server/api");
|
||||
|
||||
return api.settings.read("installedPlugins").then(function (setting) {
|
||||
should.exist(setting);
|
||||
|
||||
setting.value.should.equal('["FancyFirstChar"]')
|
||||
|
||||
done();
|
||||
});
|
||||
}, done);
|
||||
});
|
||||
|
||||
});
|
@ -25,15 +25,9 @@ describe("Ghost API", function () {
|
||||
});
|
||||
|
||||
it("uses init() to initialize", function (done) {
|
||||
var fakeDataProvider = {
|
||||
init: function () {
|
||||
return when.resolve();
|
||||
}
|
||||
},
|
||||
dataProviderInitSpy = sinon.spy(fakeDataProvider, "init"),
|
||||
oldDataProvider = ghost.dataProvider;
|
||||
|
||||
ghost.dataProvider = fakeDataProvider;
|
||||
var dataProviderInitMock = sinon.stub(ghost.dataProvider, "init", function () {
|
||||
return when.resolve();
|
||||
});
|
||||
|
||||
should.not.exist(ghost.settings());
|
||||
|
||||
@ -41,12 +35,12 @@ describe("Ghost API", function () {
|
||||
|
||||
should.exist(ghost.settings());
|
||||
|
||||
dataProviderInitSpy.called.should.equal(true);
|
||||
dataProviderInitMock.called.should.equal(true);
|
||||
|
||||
ghost.dataProvider = oldDataProvider;
|
||||
dataProviderInitMock.restore();
|
||||
|
||||
done();
|
||||
}).then(null, done);
|
||||
}, done);
|
||||
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user