Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// @internal
function getAccessKey(str) {
var i = str.indexOf("&");
if (i == -1) {
return "";
}
return str[i + 1];
}
function objectContains(o, p) {
return Object.hasOwnProperty.call(o, p);
}
// @internal
function CommandRecord(
name,
func,
usage,
help,
label,
accesskey,
flags,
keystr,
tip,
format,
helpUsage
) {
this.name = name;
this.func = func;
this._usage = usage;
this.scanUsage();
this.help = help;
this.label = label ? label : name;
this.accesskey = accesskey ? accesskey : "";
this.format = format;
this.helpUsage = helpUsage;
this.labelstr = label.replace("&", "");
this.tip = tip;
this.flags = flags;
this._enabled = true;
this.keyNodes = [];
this.keystr = keystr;
this.uiElements = [];
}
CommandRecord.prototype.__defineGetter__("enabled", cr_getenable);
function cr_getenable() {
return this._enabled;
}
CommandRecord.prototype.__defineSetter__("enabled", cr_setenable);
function cr_setenable(state) {
for (var i = 0; i < this.uiElements.length; ++i) {
if (state) {
this.uiElements[i].removeAttribute("disabled");
} else {
this.uiElements[i].setAttribute("disabled", "true");
}
}
return (this._enabled = state);
}
CommandRecord.prototype.__defineSetter__("usage", cr_setusage);
function cr_setusage(usage) {
this._usage = usage;
this.scanUsage();
}
CommandRecord.prototype.__defineGetter__("usage", cr_getusage);
function cr_getusage() {
return this._usage;
}
/**
* @internal
*
* Scans the argument spec, in the format "<a1> <a2> [<o1> <o2>]", into an
* array of strings.
*/
CommandRecord.prototype.scanUsage = function () {
var spec = this._usage;
var currentName = "";
var inName = false;
var len = spec.length;
var capNext = false;
this._usage = spec;
this.argNames = [];
for (var i = 0; i < len; ++i) {
switch (spec[i]) {
case "[":
this.argNames.push(":");
break;
case "<":
inName = true;
break;
case "-":
capNext = true;
break;
case ">":
inName = false;
this.argNames.push(currentName);
currentName = "";
capNext = false;
break;
default:
if (inName) {
currentName += capNext ? spec[i].toUpperCase() : spec[i];
}
capNext = false;
break;
}
}
};
/**
* Manages commands, with accelerator keys, help text and argument processing.
*
* You should never need to create an instance of this prototype; access the
* command manager through |client.commandManager|.
*
* @param defaultBundle An |nsIStringBundle| object to load command parameters,
* labels a help text from.
*/
function CommandManager(defaultBundle) {
this.commands = {};
this.commandHistory = {};
this.defaultBundle = defaultBundle;
this.currentDispatchDepth = 0;
this.maxDispatchDepth = 10;
this.dispatchUnwinding = false;
}
// @undocumented
CommandManager.prototype.defaultFlags = 0;
/**
* Adds multiple commands in a single call.
*
* @param cmdary |Array| containing commands to define; each item in the |Array|
* is also an |Array|, with either 3 or 4 items - corresponding to
* the first three or four arguments of |defineCommand|. An extra
* property, |stringBundle|, may be set on the |cmdary| |Array|
* to override the |defaultBundle| for all the commands.
*/
CommandManager.prototype.defineCommands = function (cmdary) {
var len = cmdary.length;
var commands = {};
var bundle = "stringBundle" in cmdary ? cmdary.stringBundle : null;
for (var i = 0; i < len; ++i) {
let name = cmdary[i][0];
let func = cmdary[i][1];
let flags = cmdary[i][2];
let usage = 3 in cmdary[i] ? cmdary[i][3] : "";
commands[name] = this.defineCommand(name, func, flags, usage, bundle);
}
return commands;
};
/**
* Adds a single command.
*
* @param name The |String| name of the command to define.
* @param func A |Function| to call to handle dispatch of the new command.
* @param flags Optional. A |Number| indicating any special requirements for the
* command.
* @param usage Optional. A |String| specifying the arguments to the command. If
* not specified, then it is assumed there are none.
* @param bundle Optional. An |nsIStringBundle| to fetch parameters, labels,
* accelerator keys and help from. If not specified, the
* |defaultBundle| is used.
*/
CommandManager.prototype.defineCommand = function (
name,
func,
flags,
usage,
bundle
) {
if (!bundle) {
bundle = this.defaultBundle;
}
var helpDefault = MSG_NO_HELP;
var labelDefault = name;
var aliasFor;
if (typeof flags != "number") {
flags = this.defaultFlags;
}
if (typeof usage != "string") {
usage = "";
}
if (typeof func == "string") {
var ary = func.match(/(\S+)/);
if (ary) {
aliasFor = ary[1];
} else {
aliasFor = null;
}
helpDefault = getMsg(MSG_DEFAULT_ALIAS_HELP, func);
if (aliasFor) {
labelDefault = getMsgFrom(
bundle,
"cmd." + aliasFor + ".label",
null,
name
);
}
}
var label = getMsgFrom(bundle, "cmd." + name + ".label", null, labelDefault);
var accesskey = getMsgFrom(
bundle,
"cmd." + name + ".accesskey",
null,
getAccessKey(label)
);
var help = helpDefault;
var helpUsage = "";
// Help is only shown for commands that available from the console.
if (flags & CMD_CONSOLE) {
help = getMsgFrom(bundle, "cmd." + name + ".help", null, helpDefault);
// Only need to lookup localized helpUsage for commands that have them.
if (usage) {
helpUsage = getMsgFrom(bundle, "cmd." + name + ".helpUsage", null, "");
}
}
var keystr = getMsgFrom(bundle, "cmd." + name + ".key", null, "");
var format = getMsgFrom(bundle, "cmd." + name + ".format", null, null);
var tip = getMsgFrom(bundle, "cmd." + name + ".tip", null, "");
var command = new CommandRecord(
name,
func,
usage,
help,
label,
accesskey,
flags,
keystr,
tip,
format,
helpUsage
);
this.addCommand(command);
if (aliasFor) {
command.aliasFor = aliasFor;
}
return command;
};
/**
* Use |defineCommand|.
*
* @internal
* @param command The |CommandRecord| to add to the |CommandManager|.
*/
CommandManager.prototype.addCommand = function (command) {
if (objectContains(this.commands, command.name)) {
/* We've already got a command with this name - invoke the history
* storage so that we can undo this back to its original state.
*/
if (!objectContains(this.commandHistory, command.name)) {
this.commandHistory[command.name] = [];
}
this.commandHistory[command.name].push(this.commands[command.name]);
}
this.commands[command.name] = command;
};
/**
* Removes multiple commands in a single call.
*
* @param cmdary An |Array| or |Object| containing |CommandRecord| objects.
* Ideally use the value returned from |defineCommands|.
*/
CommandManager.prototype.removeCommands = function (cmdary) {
for (var i in cmdary) {
var command = isinstance(cmdary[i], Array)
? { name: cmdary[i][0] }
: cmdary[i];
this.removeCommand(command);
}
};
/**
* Removes a single command.
*
* @param command The |CommandRecord| to remove from the |CommandManager|.
* Ideally use the value returned from |defineCommand|.
*/
CommandManager.prototype.removeCommand = function (command) {
delete this.commands[command.name];
if (objectContains(this.commandHistory, command.name)) {
/* There was a previous command with this name - restore the most
* recent from the history, returning the command to its former glory.
*/
this.commands[command.name] = this.commandHistory[command.name].pop();
if (this.commandHistory[command.name].length == 0) {
delete this.commandHistory[command.name];
}
}
};
/**
* Registers a hook for a particular command.
*
* A command hook is uniquely identified by the pair |id|, |before|; only a
* single hook may exist for a given pair of |id| and |before| values. It is
* wise to use a unique |id|; plugins should construct an |id| using
* |plugin.id|, e.g. |plugin.id + "-my-hook-1"|.
*
* @param commandName A |String| command name to hook. The command named must
* already exist in the |CommandManager|; if it does not, no
* hook is added.
* @param func A |Function| to handle the hook.
* @param id A |String| identifier for the hook.
* @param before A |Boolean| indicating whether the hook wishes to be
* called before or after the command executes.
*/
CommandManager.prototype.addHook = function (commandName, func, id, before) {
if (
!ASSERT(
objectContains(this.commands, commandName),
"Unknown command '" + commandName + "'"
)
) {
return;
}
var command = this.commands[commandName];
if (before) {
if (!("beforeHooks" in command)) {
command.beforeHooks = {};
}
command.beforeHooks[id] = func;
} else {
if (!("afterHooks" in command)) {
command.afterHooks = {};
}
command.afterHooks[id] = func;
}
};
/**
* Registers multiple hooks for commands.
*
* @param hooks An |Object| containing |Function| objects to call for each
* hook; the key of each item is the name of the command it
* wishes to hook. Optionally, the |_before| property can be
* added to a |function| to override the default |before| value
* of |false|.
* @param prefix Optional. A |String| prefix to apply to each hook's command
* name to compute an |id| for it.
*/
CommandManager.prototype.addHooks = function (hooks, prefix) {
if (!prefix) {
prefix = "";
}
for (var h in hooks) {
this.addHook(
h,
hooks[h],
prefix + ":" + h,
"_before" in hooks[h] ? hooks[h]._before : false
);
}
};
/**
* Unregisters multiple hooks for commands.
*
* @param hooks An |Object| identical to the one passed to |addHooks|.
* @param prefix Optional. A |String| identical to the one passed to |addHooks|.
*/
CommandManager.prototype.removeHooks = function (hooks, prefix) {
if (!prefix) {
prefix = "";
}
for (var h in hooks) {
this.removeHook(
h,
prefix + ":" + h,
"before" in hooks[h] ? hooks[h].before : false
);
}
};
/**
* Unregisters a hook for a particular command.
*
* The arguments to |removeHook| are the same as |addHook|, but without the
* hook function itself.
*
* @param commandName The |String| command name to unhook.
* @param id The |String| identifier for the hook.
* @param before A |Boolean| indicating whether the hook was to be
* called before or after the command executed.
*/
CommandManager.prototype.removeHook = function (commandName, id, before) {
var command = this.commands[commandName];
if (before) {
delete command.beforeHooks[id];
} else {
delete command.afterHooks[id];
}
};
/**
* Gets a sorted |Array| of |CommandRecord| objects which match.
*
* After filtering by |flags| (if specified), if an exact match for
* |partialName| is found, only that is returned; otherwise, all commands
* starting with |partialName| are returned in alphabetical order by |label|.
*
* @param partialName Optional. A |String| prefix to search for.
* @param flags Optional. Flags to logically AND with commands.
*/
CommandManager.prototype.list = function (partialName, flags, exact) {
/* returns array of command objects which look like |partialName|, or
* all commands if |partialName| is not specified */
function compare(a, b) {
a = a.labelstr.toLowerCase();
b = b.labelstr.toLowerCase();
if (a == b) {
return 0;
}
if (a > b) {
return 1;
}
return -1;
}
var ary = [];
var commandNames = Object.keys(this.commands);
for (var name of commandNames) {
let command = this.commands[name];
if (
(!flags || command.flags & flags) &&
(!partialName || command.name.startsWith(partialName))
) {
if (exact && partialName && partialName.length == command.name.length) {
/* exact match */
return [command];
}
ary.push(command);
}
}
ary.sort(compare);
return ary;
};
/**
* Gets a sorted |Array| of command names which match.
*
* |listNames| operates identically to |list|, except that only command names
* are returned, not |CommandRecord| objects.
*/
CommandManager.prototype.listNames = function (partialName, flags) {
var cmds = this.list(partialName, flags, false);
var cmdNames = [];
for (var c in cmds) {
cmdNames.push(cmds[c].name);
}
cmdNames.sort();
return cmdNames;
};
/**
* Internal use only.
*
* Called to parse the arguments stored in |e.inputData|, as properties of |e|,
* for the CommandRecord stored on |e.command|.
*
* @params e Event object to be processed.
*/
// @undocumented
CommandManager.prototype.parseArguments = function (e) {
var rv = this.parseArgumentsRaw(e);
//dd("parseArguments '" + e.command.usage + "' " +
// (rv ? "passed" : "failed") + "\n" + dumpObjectTree(e));
delete e.currentArgIndex;
return rv;
};
/**
* Internal use only.
*
* Don't call parseArgumentsRaw directly, use parseArguments instead.
*
* Parses the arguments stored in the |inputData| property of the event object,
* according to the format specified by the |command| property.
*
* On success this method returns true, and propery names corresponding to the
* argument names used in the format spec will be created on the event object.
* All optional parameters will be initialized to |null| if not already present
* on the event.
*
* On failure this method returns false and a description of the problem
* will be stored in the |parseError| property of the event.
*
* For example...
* Given the argument spec "<int> <word> [ <word2> <word3> ]", and given the
* input string "411 foo", stored as |e.command.usage| and |e.inputData|
* respectively, this method would add the following propertys to the event
* object...
* -name---value--notes-
* e.int 411 Parsed as an integer
* e.word foo Parsed as a string
* e.word2 null Optional parameters not specified will be set to null.
* e.word3 null If word2 had been provided, word3 would be required too.
*
* Each parameter is parsed by calling the function with the same name, located
* in this.argTypes. The first parameter is parsed by calling the function
* this.argTypes["int"], for example. This function is expected to act on
* e.unparsedData, taking it's chunk, and leaving the rest of the string.
* The default parse functions are...
* <word> parses contiguous non-space characters.
* <int> parses as an int.
* <rest> parses to the end of input data.
* <state> parses yes, on, true, 1, 0, false, off, no as a boolean.
* <toggle> parses like a <state>, except allows "toggle" as well.
* <...> parses according to the parameter type before it, until the end
* of the input data. Results are stored in an array named
* paramnameList, where paramname is the name of the parameter
* before <...>. The value of the parameter before this will be
* paramnameList[0].
*
* If there is no parse function for an argument type, "word" will be used by
* default. You can alias argument types with code like...
* commandManager.argTypes["my-integer-name"] = commandManager.argTypes["int"];
*/
// @undocumented
CommandManager.prototype.parseArgumentsRaw = function (e) {
var argc = e.command.argNames.length;
function initOptionals() {
for (var i = 0; i < argc; ++i) {
if (
e.command.argNames[i] != ":" &&
e.command.argNames[i] != "..." &&
!(e.command.argNames[i] in e)
) {
e[e.command.argNames[i]] = null;
}
if (e.command.argNames[i] == "...") {
var paramName = e.command.argNames[i - 1];
if (paramName == ":") {
paramName = e.command.argNames[i - 2];
}
var listName = paramName + "List";
if (!(listName in e)) {
e[listName] = [e[paramName]];
}
}
}
}
if ("inputData" in e && e.inputData) {
/* if data has been provided, parse it */
e.unparsedData = e.inputData;
var parseResult;
var currentArg;
e.currentArgIndex = 0;
if (argc) {
currentArg = e.command.argNames[e.currentArgIndex];
while (e.unparsedData) {
if (currentArg != ":") {
if (!this.parseArgument(e, currentArg)) {
return false;
}
}
if (++e.currentArgIndex < argc) {
currentArg = e.command.argNames[e.currentArgIndex];
} else {
break;
}
}
if (e.currentArgIndex < argc && currentArg != ":") {
/* parse loop completed because it ran out of data. We haven't
* parsed all of the declared arguments, and we're not stopped
* at an optional marker, so we must be missing something
* required... */
e.parseError = getMsg(
MSG_ERR_REQUIRED_PARAM,
e.command.argNames[e.currentArgIndex]
);
return false;
}
}
if (e.unparsedData) {
/* parse loop completed with unparsed data, which means we've
* successfully parsed all arguments declared. Whine about the
* extra data... */
display(getMsg(MSG_EXTRA_PARAMS, e.unparsedData), MT_WARN);
}
}
var rv = this.isCommandSatisfied(e);
if (rv) {
initOptionals();
}
return rv;
};
/**
* Returns true if |e| has the properties required to call the command
* |command|.
*
* If |command| is not provided, |e.command| is used instead.
*
* @param e Event object to test against the command.
* @param command Command to test.
*/
// @undocumented
CommandManager.prototype.isCommandSatisfied = function (e, command) {
if (typeof command == "undefined") {
command = e.command;
} else if (typeof command == "string") {
command = this.commands[command];
}
if (!command.enabled) {
return false;
}
for (var i = 0; i < command.argNames.length; ++i) {
if (command.argNames[i] == ":") {
return true;
}
if (!(command.argNames[i] in e)) {
e.parseError = getMsg(MSG_ERR_REQUIRED_PARAM, command.argNames[i]);
//dd("command '" + command.name + "' unsatisfied: " + e.parseError);
return false;
}
}
//dd ("command '" + command.name + "' satisfied.");
return true;
};
/**
* Internal use only.
* See parseArguments above and the |argTypes| object below.
*
* Parses the next argument by calling an appropriate parser function, or the
* generic "word" parser if none other is found.
*
* @param e event object.
* @param name property name to use for the parse result.
*/
// @undocumented
CommandManager.prototype.parseArgument = function (e, name) {
var parseResult;
if (name in this.argTypes) {
parseResult = this.argTypes[name](e, name, this);
} else {
parseResult = this.argTypes.word(e, name, this);
}
if (!parseResult) {
e.parseError = getMsg(MSG_ERR_INVALID_PARAM, [name, e.unparsedData]);
}
return parseResult;
};
// @undocumented
CommandManager.prototype.argTypes = {};
/**
* Convenience function used to map a list of new types to an existing parse
* function.
*/
// @undocumented
CommandManager.prototype.argTypes.__aliasTypes__ = function (list, type) {
for (var i in list) {
this[list[i]] = this[type];
}
};
/**
* Internal use only.
*
* Parses an integer, stores result in |e[name]|.
*/
// @undocumented
CommandManager.prototype.argTypes.int = function (e, name) {
var ary = e.unparsedData.match(/(\d+)(?:\s+(.*))?$/);
if (!ary) {
return false;
}
e[name] = Number(ary[1]);
e.unparsedData = arrayHasElementAt(ary, 2) ? ary[2] : "";
return true;
};
/**
* Internal use only.
*
* Parses a word, which is defined as a list of nonspace characters.
*
* Stores result in |e[name]|.
*/
// @undocumented
CommandManager.prototype.argTypes.word = function (e, name) {
var ary = e.unparsedData.match(/(\S+)(?:\s+(.*))?$/);
if (!ary) {
return false;
}
e[name] = ary[1];
e.unparsedData = arrayHasElementAt(ary, 2) ? ary[2] : "";
return true;
};
/**
* Internal use only.
*
* Parses a "state" which can be "true", "on", "yes", or 1 to indicate |true|,
* or "false", "off", "no", or 0 to indicate |false|.
*
* Stores result in |e[name]|.
*/
// @undocumented
CommandManager.prototype.argTypes.state = function (e, name) {
var ary = e.unparsedData.match(
/(true|on|yes|1|false|off|no|0)(?:\s+(.*))?$/i
);
if (!ary) {
return false;
}
if (ary[1].search(/true|on|yes|1/i) != -1) {
e[name] = true;
} else {
e[name] = false;
}
e.unparsedData = arrayHasElementAt(ary, 2) ? ary[2] : "";
return true;
};
/**
* Internal use only.
*
* Parses a "toggle" which can be "true", "on", "yes", or 1 to indicate |true|,
* or "false", "off", "no", or 0 to indicate |false|. In addition, the string
* "toggle" is accepted, in which case |e[name]| will be the string "toggle".
*
* Stores result in |e[name]|.
*/
// @undocumented
CommandManager.prototype.argTypes.toggle = function (e, name) {
var ary = e.unparsedData.match(
/(toggle|true|on|yes|1|false|off|no|0)(?:\s+(.*))?$/i
);
if (!ary) {
return false;
}
if (ary[1].search(/toggle/i) != -1) {
e[name] = "toggle";
} else if (ary[1].search(/true|on|yes|1/i) != -1) {
e[name] = true;
} else {
e[name] = false;
}
e.unparsedData = arrayHasElementAt(ary, 2) ? ary[2] : "";
return true;
};
/**
* Internal use only.
*
* Returns all unparsed data to the end of the line.
*
* Stores result in |e[name]|.
*/
// @undocumented
CommandManager.prototype.argTypes.rest = function (e, name) {
e[name] = e.unparsedData;
e.unparsedData = "";
return true;
};
/**
* Internal use only.
*
* Parses the rest of the unparsed data the same way the previous argument was
* parsed. Can't be used as the first parameter. if |name| is "..." then the
* name of the previous argument, plus the suffix "List" will be used instead.
*
* Stores result in |e[name]| or |e[lastName + "List"]|.
*/
// @undocumented
CommandManager.prototype.argTypes["..."] = function (e, name, cm) {
ASSERT(e.currentArgIndex > 0, "<...> can't be the first argument.");
var lastArg = e.command.argNames[e.currentArgIndex - 1];
if (lastArg == ":") {
lastArg = e.command.argNames[e.currentArgIndex - 2];
}
var listName = lastArg + "List";
e[listName] = [e[lastArg]];
while (e.unparsedData) {
if (!cm.parseArgument(e, lastArg)) {
return false;
}
e[listName].push(e[lastArg]);
}
e[lastArg] = e[listName][0];
return true;
};