/**
* Handles extensions. If scripts should be managed by dwbem they must
* be implemented as an extension. In contrast to regular scripts extensions can
* be enabled/disabled on the fly without reloading all userscripts. Every extension must contain two special tags,
* /*<INFO ... INFO>*/ that will be used by dwbem to find information
* about the extension and //<DEFAULT_CONFIG ... //>DEFAULT_CONFIG that
* will be used by dwbem to find the default configuration
* Every extension must also return an object that can have up to four properties.
*
*
* @namespace
* Static object that handles extensions
* @name extensions
* @static
*
* @property {Object} [defaultConfig]
* The default configuration, will be passed to extensions.getConfig
* @property {extensions~onEnd} end
* A function that will be called when the extension is unloaded, can be
* used to disconnect from signals/unbind shortcuts, ...
* @property {Object} [exports]
* An object that exports some functionality or the configuration, the
* object can be retrieved in other scripts with {@link require}
* @property {extensions~onInit} init
* A function that will be called when an extension is loaded
*
* @example
* // ext:set ft=javascript:
*
* /*<INFO
* Extension that does some awesome things
* INFO>*/
*
* var defaultConfig = {
* //<DEFAULT_CONFIG
* // Foo
* foo : 37,
* // Shortcut to do some action
* shortcut : "foo"
* //>DEFAULT_CONFIG
* };
*
* var myConfig = {};
*
* function action() {
* ...
* }
* function onNavigation() {
* ...
* }
* var myExtension = {
* defaultConfig : defaultConfig,
* exports : {
* action : action
* },
* init : function(config) {
* myConfig = config;
* bind(config.shortcut, "action");
* signals.connect("navigation", onNavigation);
* return true;
* },
* end : function() {
* signals.disconnect(onNavigation);
* unbind(action);
* return true;
* }
* };
* return myExtension;
*
* */
/**
* Called when an extension is unloaded
* @callback extensions~onEnd
*
* @returns {Boolean}
* Return true if the extension was successfully unloaded
* */
/**
* Called when an extension is loaded
* @callback extensions~onInit
*
* @param {Object} configuration
* The configuration passed to {@link extensions.load}
* @returns {Boolean or Deferred}
* Return true if the extension was successfully initialized, for
* asynchronous initializations the callback can return a {@link Deferred}, to
* indicate a successful initialization either call {@linkcode deferred.resolve(true)}, to
* indicate an error either call {@linkcode deferred.resolve(false)} or
* {@linkcode deferred.reject(reason)} on the deferred
* */
(function () {
var _config = {};
var _registered = {};
var _configLoaded = false;
var getPlugin = function(name, filename)
{
var ret = null;
try
{
if (system.fileTest(filename, FileTest.exists))
ret = include(filename);
}
catch(e)
{
extensions.error(name, "Error in line " + e.line + " parsing " + filename);
}
return ret;
};
var getStack = function(offset)
{
if (arguments.length === 0)
offset = 0;
try
{
throw Error (message);
}
catch (e)
{
var stack = e.stack.match(/[^\n]+/g);
return "STACK: [" + stack.slice(offset+1).join("] [")+"]";
}
};
var _unload = function (name, removeConfig)
{
if (_registered[name] !== undefined)
{
if (_registered[name].end instanceof Function)
{
_registered[name].end();
extensions.message(name, "Extension unloaded.");
}
if (removeConfig)
delete _config[name];
delete _registered[name];
provide(name, null, true);
return true;
}
return false;
};
Object.defineProperties(extensions,
{
/**
* Print a warning message to stderr
*
* @memberOf extensions
* @function
*
* @param {String} name
* Name of the extension
* @param {String} message
* The message to print
* */
"warning" :
{
value : function (name, message)
{
io.print("\033[1mDWB EXTENSION WARNING: \033[0mextension \033[1m" + name + "\033[0m: " + message, "stderr");
}
},
/**
* Print an error message to stderr
*
* @memberOf extensions
* @function
*
* @param {String} name
* Name of the extension
* @param {String} message
* The message to print
* */
"error" :
{
value : function (name, a, b) {
var message = "";
if (a instanceof Error) {
if (a.message) {
message = a.message;
}
else if (arguments.length > 2)
message = b;
else
b = "";
io.print("\033[31mDWB EXTENSION ERROR: \033[0mextension \033[1m" + name + "\033[0m in line " + a.line + ": " +
message + "\nSTACK: [" + a.stack.match(/[^\n]+/g).join("] [") + "]", "stderr");
}
else {
io.print("\033[31mDWB EXTENSION ERROR: \033[0mextension \033[1m" + name + "\033[0m: " + a + "\n" + getStack(1), "stderr");
}
}
},
/**
* Print message to stderr
*
* @memberOf extensions
* @function
*
* @param {String} name
* Name of the extension
* @param {String} message
* The message to print
* */
"message" :
{
value : function (name, message)
{
io.print("\033[1mDWB EXTENSION: \033[0mextension \033[1m" + name + "\033[0m: " + message, "stderr");
}
},
/**
* @name getConfig
* @memberOf extensions
* @deprecated use {@link util.mixin}
* @function
* */
"getConfig" :
{
value : function(c, dc)
{
return _deprecated("extensions.getConfig", "util.mixin", arguments);
}
},
/**
* Loads an extension, the default path for an extension is
* {@link data.userDataDir}/extensions/name_of_extension or
* {@link data.systemDataDir}/extensions/name_of_extension
*
* @memberOf extensions
* @function
*
* @param {String} name
* The name of the extension
* @param {Object} configuration
* The configuration that will be used for the extension
* */
"load" :
{
value : function(name, c)
{
if (_registered[name] !== undefined)
extensions.error(name, "Already loaded.");
var boldname = "\033[1m" + name + "\033[0m";
var config, dataBase, pluginPath, plugin = null, key, filename;
var extConfig = null;
/* Get default config if the config hasn't been read yet */
if (arguments.length == 2)
{
extConfig = c;
_config[name] = c;
}
if (!_configLoaded)
{
if (system.fileTest(data.configDir + "/extensionrc", FileTest.regular))
{
try
{
config = include(data.configDir + "/extensionrc");
}
catch (e)
{
extensions.error(name, "loading config failed : " + e);
}
if (config === null)
{
extensions.warning(name, "Could not load config.");
}
else
{
for (key in config)
_config[key] = config[key];
}
_configLoaded = true;
}
}
if (extConfig === null)
extConfig = _config[name] || null;
/* Load extension */
if (data.userDataDir)
{
filename = data.userDataDir + "/extensions/" + name;
plugin = getPlugin(name, data.userDataDir + "/extensions/" + name);
}
if (plugin === null)
{
plugin = getPlugin(name, data.systemDataDir + "/extensions/" + name);
if (plugin === null)
{
extensions.error(name, "Couldn't find extension.");
return;
}
}
try
{
plugin._name = name;
if (plugin.defaultConfig)
util.mixin(extConfig, plugin.defaultConfig);
Deferred.when(plugin.init(extConfig), function(success) {
if (success)
{
_registered[name] = plugin;
if (plugin.exports)
provide(name, plugin.exports, true);
extensions.message(name, "Successfully loaded and initialized.");
}
else
{
extensions.error(name, "Initialization failed.");
}
}, function(reason) {
if (reason)
extensions.error(name, "Initialization failed: " + reason);
else
extensions.error(name, "Initialization failed.");
});
}
catch (e)
{
extensions.error(name, "Initialization failed: " + e);
}
}
},
/**
* Unloads an extension, calls extension.end when the extensions is
* unloaded
*
* @memberOf extensions
* @function
*
* @param {String} name
* The name of the extension
*
* @returns {Boolean}
* true if the extension was found and unloaded
* */
"unload" :
{
value : function(name)
{
return _unload(name, true);
}
},
/**
* Disables all extensions, calls {@link extensions.unload} for every
* extension
*
* @memberOf extensions
* @function
*
* */
"disableAll" :
{
value : function()
{
for (var key in _registered)
_unload(key, true);
}
},
/**
* Toggles an extension, if it is loaded toggle will unload it, otherwise
* it will load it.
*
* @memberOf extensions
* @function
*
* @param {String} name
* Name of the extension
* @param {Object} configuration
* Configuration that will be passed to {@link extensions.load}
*
* @returns {Boolean}
* true if the extension was loaded, false if it was unloaded
* */
"toggle" :
{
value : function(name, c)
{
if (_registered[name] !== undefined)
{
_unload(name);
return false;
}
else
{
extensions.load(name, c);
return true;
}
}
},
/**
* Binds an extension to a shortcut
*
* @memberOf extensions
* @function
*
* @param {String} name
* Name of the extension
* @param {String} shortcut
* The shortcut that will be used to toggle the extension
* @param {Object} options
* @param {Boolean} options.load
* Whether to initially load the extension
* @param {Boolean} options.config
* The configuration passed to {@link extensions.load}
* @param {String} options.command
* Command that can be used from dwb's command line to toggle the
* extension
* */
"bind" :
{
value : function(name, shortcut, options)
{
if (!name || !shortcut)
return;
if (options.load === undefined || options.load)
extensions.load(name, options.config);
bind(shortcut, function () {
if (extensions.toggle(name, options.config))
io.notify("Extension " + name + " enabled");
else
io.notify("Extension " + name + " disabled");
}, options.command);
}
}
});
})();
Object.freeze(extensions);