/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- * * ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is JSIRC Library. * * The Initial Developer of the Original Code is * New Dimensions Consulting, Inc. * Portions created by the Initial Developer are Copyright (C) 1999 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Robert Ginda, rginda@ndcico.com, original author * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ function getAccessKey (str) { var i = str.indexOf("&"); if (i == -1) return ""; return str[i + 1]; } function CommandRecord (name, func, usage, help, label, flags, keystr, tip, format) { this.name = name; this.func = func; this._usage = usage; this.scanUsage(); this.help = help; this.label = label ? label : name; this.format = format; this.labelstr = label.replace ("&", ""); this.tip = tip; this.flags = flags; this._enabled = true; this.keyNodes = new Array(); this.keystr = keystr; this.uiElements = new Array(); } 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 use only. * * Scans the argument spec, in the format " [ ]", into an * array of strings. */ CommandRecord.prototype.scanUsage = function cr_scanusage() { var spec = this._usage; var currentName = ""; var inName = false; var len = spec.length; var capNext = false; this._usage = spec; this.argNames = new Array(); 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; } } } /** * Returns the command formatted for presentation as part of online * documentation. */ CommandRecord.prototype.getDocumentation = function cr_getdocs(flagFormatter) { var str; str = getMsg(MSG_DOC_COMMANDLABEL, [this.label.replace("&", ""), this.name]) + "\n"; str += getMsg(MSG_DOC_KEY, this.keystr ? this.keystr : MSG_VAL_NA) + "\n"; str += getMsg(MSG_DOC_SYNTAX, [this.name, this.usage]) + "\n"; str += MSG_DOC_NOTES + "\n"; str += (flagFormatter ? flagFormatter(this.flags) : this.flags) + "\n\n"; str += MSG_DOC_DESCRIPTION + "\n"; str += wrapText(this.help, 75) + "\n"; return str; } CommandRecord.prototype.argNames = new Array(); function CommandManager (defaultBundle) { this.commands = new Object(); this.defaultBundle = defaultBundle; this.currentDispatchDepth = 0; this.maxDispatchDepth = 10; this.dispatchUnwinding = false; } CommandManager.prototype.defaultFlags = 0; CommandManager.prototype.defineCommands = function cmgr_defcmds (cmdary) { var len = cmdary.length; var commands = new Object(); var bundle = "stringBundle" in cmdary ? cmdary.stringBundle : null; for (var i = 0; i < len; ++i) { var name = cmdary[i][0]; var func = cmdary[i][1]; var flags = cmdary[i][2]; var usage; if (3 in cmdary[i]) usage = cmdary[i][3]; var command = this.defineCommand(name, func, flags, usage, bundle); commands[name] = command; } return commands; } CommandManager.prototype.defineCommand = function cmdmgr_defcmd (name, func, flags, usage, bundle) { if (!bundle) bundle = this.defaultBundle; var helpDefault; var labelDefault = name; var aliasFor; if (typeof flags != "number") flags = this.defaultFlags; if (flags & CMD_NO_HELP) helpDefault = MSG_NO_HELP; if (typeof usage != "string") usage = getMsgFrom(bundle, "cmd." + name + ".params", null, ""); 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 help = getMsgFrom(bundle, "cmd." + name + ".help", null, helpDefault); 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, flags, keystr, tip, format); this.addCommand(command); if (aliasFor) command.aliasFor = aliasFor; return command; } CommandManager.prototype.installKeys = function cmgr_instkeys (document, commands) { var parentElem = document.getElementById("dynamic-keys"); if (!parentElem) { parentElem = document.createElement("keyset"); parentElem.setAttribute("id", "dynamic-keys"); document.documentElement.appendChild(parentElem); } if (!commands) commands = this.commands; for (var c in commands) this.installKey (parentElem, commands[c]); } /** * Create a node relative to a DOM node. Usually called once per command, * per document, so that accelerator keys work in all application windows. * * @param parentElem A reference to the DOM node which should contain the new * node. * @param command reference to the CommandRecord to install. */ CommandManager.prototype.installKey = function cmgr_instkey (parentElem, command) { if (!command.keystr) return; var ary = command.keystr.match (/(.*\s)?([\S]+)$/); if (!ASSERT(ary, "couldn't parse key string ``" + command.keystr + "'' for command ``" + command.name + "''")) { return; } var key = document.createElement ("key"); key.setAttribute ("id", "key:" + command.name); key.setAttribute ("oncommand", "dispatch('" + command.name + "', {isInteractive: true});"); if (ary[1]) key.setAttribute ("modifiers", ary[1]); if (ary[2].indexOf("VK_") == 0) key.setAttribute ("keycode", ary[2]); else key.setAttribute ("key", ary[2]); parentElem.appendChild(key); command.keyNodes.push(key); } CommandManager.prototype.uninstallKeys = function cmgr_uninstkeys (commands) { if (!commands) commands = this.commands; for (var c in commands) this.uninstallKey (commands[c]); } CommandManager.prototype.uninstallKey = function cmgr_uninstkey (command) { for (var i in command.keyNodes) { try { /* document may no longer exist in a useful state. */ command.keyNodes[i].parentNode.removeChild(command.keyNodes[i]); } catch (ex) { dd ("*** caught exception uninstalling key node: " + ex); } } } /** * Register a new command with the manager. */ CommandManager.prototype.addCommand = function cmgr_add (command) { this.commands[command.name] = command; } CommandManager.prototype.removeCommands = function cmgr_removes (cmdary) { for (var i in cmdary) { var command = isinstance(cmdary[i], Array) ? {name: cmdary[i][0]} : cmdary[i]; this.removeCommand(command); } } CommandManager.prototype.removeCommand = function cmgr_remove (command) { delete this.commands[command.name]; } /** * Register a hook for a particular command name. |id| is a human readable * identifier for the hook, and can be used to unregister the hook. If you * re-use a hook id, the previous hook function will be replaced. * If |before| is |true|, the hook will be called *before* the command executes, * if |before| is |false|, or not specified, the hook will be called *after* * the command executes. */ CommandManager.prototype.addHook = function cmgr_hook (commandName, func, id, before) { if (!ASSERT(commandName in this.commands, "Unknown command '" + commandName + "'")) { return; } var command = this.commands[commandName]; if (before) { if (!("beforeHooks" in command)) command.beforeHooks = new Object(); command.beforeHooks[id] = func; } else { if (!("afterHooks" in command)) command.afterHooks = new Object(); command.afterHooks[id] = func; } } CommandManager.prototype.addHooks = function cmgr_hooks (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); } } CommandManager.prototype.removeHooks = function cmgr_remhooks (hooks, prefix) { if (!prefix) prefix = ""; for (var h in hooks) { this.removeHook(h, prefix + ":" + h, ("before" in hooks[h]) ? hooks[h].before : false); } } CommandManager.prototype.removeHook = function cmgr_unhook (commandName, id, before) { var command = this.commands[commandName]; if (before) delete command.beforeHooks[id]; else delete command.afterHooks[id]; } /** * Return an array of all CommandRecords which start with the string * |partialName|, sorted by |label| property. * * @param partialName Prefix to search for. * @param flags logical ANDed with command flags. * @returns array Array of matching commands, sorted by |label| property. */ CommandManager.prototype.list = function cmgr_list (partialName, flags) { /* 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 = new Array(); var commandNames = keys(this.commands); /* A command named "eval" wouldn't show up in the result of keys() because * eval is not-enumerable, even if overwritten, in Mozilla 1.0. */ if (("eval" in this.commands) && (typeof this.commands.eval == "object") && !arrayContains(commandNames, "eval")) { commandNames.push("eval"); } for (var i in commandNames) { var name = commandNames[i]; if (!flags || (this.commands[name].flags & flags)) { if (!partialName || this.commands[name].name.indexOf(partialName) == 0) { if (partialName && partialName.length == this.commands[name].name.length) { /* exact match */ return [this.commands[name]]; } else { ary.push (this.commands[name]); } } } } ary.sort(compare); return ary; } /** * Return a sorted array of the command names which start with the string * |partialName|. * * @param partialName Prefix to search for. * @param flags logical ANDed with command flags. * @returns array Sorted Array of matching command names. */ CommandManager.prototype.listNames = function cmgr_listnames (partialName, flags) { var cmds = this.list(partialName, flags); var cmdNames = new Array(); 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. */ CommandManager.prototype.parseArguments = function cmgr_parseargs (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 " [ ]", 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... * parses contiguous non-space characters. * parses as an int. * parses to the end of input data. * parses yes, on, true, 1, 0, false, off, no as a boolean. * parses like a , 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"]; */ CommandManager.prototype.parseArgumentsRaw = function parse_parseargsraw (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. */ CommandManager.prototype.isCommandSatisfied = function cmgr_isok (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. */ CommandManager.prototype.parseArgument = function cmgr_parsearg (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; } CommandManager.prototype.argTypes = new Object(); /** * Convenience function used to map a list of new types to an existing parse * function. */ CommandManager.prototype.argTypes.__aliasTypes__ = function at_alias (list, type) { for (var i in list) { this[list[i]] = this[type]; } } /** * Internal use only. * * Parses an integer, stores result in |e[name]|. */ CommandManager.prototype.argTypes["int"] = function parse_int (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]|. */ CommandManager.prototype.argTypes["word"] = function parse_word (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]|. */ CommandManager.prototype.argTypes["state"] = function parse_state (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]|. */ CommandManager.prototype.argTypes["toggle"] = function parse_toggle (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]|. */ CommandManager.prototype.argTypes["rest"] = function parse_rest (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"]|. */ CommandManager.prototype.argTypes["..."] = function parse_repeat (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; }