/* * Copyright 2009-2011 Mozilla Foundation and contributors * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ /* * * * * * * * *********************************** WARNING *********************************** * * Do not edit this file without understanding where it comes from, * Your changes are likely to be overwritten without warning. * * For more information on GCLI see: * - https://github.com/mozilla/gcli/blob/master/docs/index.md * - https://wiki.mozilla.org/DevTools/Features/GCLI * * The original source for this file is: * https://github.com/mozilla/gcli/ * * This build of GCLI for Firefox comes from 4 bits of code: * - prefix-gcli.jsm: Initial commentary and EXPORTED_SYMBOLS * - console.js: Support code common to web content that is not part of the * default firefox chrome environment and is easy to shim. * - mini_require: A very basic commonjs AMD (Asynchronous Modules Definition) * 'require' implementation (which is just good enough to load GCLI). For * more, see http://wiki.commonjs.org/wiki/Modules/AsynchronousDefinition. * This alleviates the need for requirejs (http://requirejs.org/) which is * used when running in the browser. This code is provided by dryice. * - A build of GCLI itself, packaged using dryice * - suffix-gcli.jsm - code to require the gcli object for EXPORTED_SYMBOLS. * * See gcli.js for more details of this build. * For more details on dryice, see the https://github.com/mozilla/dryice * ******************************************************************************* * * * * * * * * * */ /////////////////////////////////////////////////////////////////////////////// var EXPORTED_SYMBOLS = [ "gcli" ]; /** * Expose Node/HTMLElement objects. This allows us to use the Node constants * without resorting to hardcoded numbers */ var Node = Components.interfaces.nsIDOMNode; var HTMLElement = Components.interfaces.nsIDOMHTMLElement; Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); /** * Define setTimeout and clearTimeout to match the browser functions */ var setTimeout; var clearTimeout; (function() { /** * The next value to be returned by setTimeout */ var nextID = 1; /** * The map of outstanding timeouts */ var timers = {}; /** * Object to be passed to Timer.initWithCallback() */ function TimerCallback(callback) { this._callback = callback; var interfaces = [ Components.interfaces.nsITimerCallback ]; this.QueryInterface = XPCOMUtils.generateQI(interfaces); } TimerCallback.prototype.notify = function(timer) { try { for (var timerID in timers) { if (timers[timerID] === timer) { delete timers[timerID]; break; } } this._callback.apply(null, []); } catch (ex) { console.error(ex); } }; /** * Executes a code snippet or a function after specified delay. * This is designed to have the same interface contract as the browser * function. * @param callback is the function you want to execute after the delay. * @param delay is the number of milliseconds that the function call should * be delayed by. Note that the actual delay may be longer, see Notes below. * @return the ID of the timeout, which can be used later with * window.clearTimeout. */ setTimeout = function setTimeout(callback, delay) { var timer = Components.classes["@mozilla.org/timer;1"] .createInstance(Components.interfaces.nsITimer); var timerID = nextID++; timers[timerID] = timer; timer.initWithCallback(new TimerCallback(callback), delay, timer.TYPE_ONE_SHOT); return timerID; }; /** * Clears the delay set by window.setTimeout() and prevents the callback from * being executed (if it hasn't been executed already) * @param timerID the ID of the timeout you wish to clear, as returned by * window.setTimeout(). */ clearTimeout = function clearTimeout(timerID) { var timer = timers[timerID]; if (timer) { timer.cancel(); delete timers[timerID]; } }; })(); /** * This creates a console object that somewhat replicates Firebug's console * object. It currently writes to dump(), but should write to the web * console's chrome error section (when it has one) */ var console = {}; (function() { /** * String utility to ensure that strings are a specified length. Strings * that are too long are truncated to the max length and the last char is * set to "_". Strings that are too short are left padded with spaces. * * @param {string} aStr * The string to format to the correct length * @param {number} aMaxLen * The maximum allowed length of the returned string * @param {number} aMinLen (optional) * The minimum allowed length of the returned string. If undefined, * then aMaxLen will be used * @param {object} aOptions (optional) * An object allowing format customization. The only customization * allowed currently is 'truncate' which can take the value "start" to * truncate strings from the start as opposed to the end. * @return {string} * The original string formatted to fit the specified lengths */ function fmt(aStr, aMaxLen, aMinLen, aOptions) { if (aMinLen == null) { aMinLen = aMaxLen; } if (aStr == null) { aStr = ""; } if (aStr.length > aMaxLen) { if (aOptions && aOptions.truncate == "start") { return "_" + aStr.substring(aStr.length - aMaxLen + 1); } else { return aStr.substring(0, aMaxLen - 1) + "_"; } } if (aStr.length < aMinLen) { return Array(aMinLen - aStr.length + 1).join(" ") + aStr; } return aStr; } /** * Utility to extract the constructor name of an object. * Object.toString gives: "[object ?????]"; we want the "?????". * * @param {object} aObj * The object from which to extract the constructor name * @return {string} * The constructor name */ function getCtorName(aObj) { if (aObj.constructor && aObj.constructor.name) { return aObj.constructor.name; } // If that fails, use Objects toString which sometimes gives something // better than 'Object', and at least defaults to Object if nothing better return Object.prototype.toString.call(aObj).slice(8, -1); } /** * A single line stringification of an object designed for use by humans * * @param {any} aThing * The object to be stringified * @return {string} * A single line representation of aThing, which will generally be at * most 80 chars long */ function stringify(aThing) { if (aThing === undefined) { return "undefined"; } if (aThing === null) { return "null"; } if (typeof aThing == "object") { var type = getCtorName(aThing); if (aThing instanceof Node && aThing.tagName) { return debugElement(aThing); } type = (type == "Object" ? "" : type + " "); var json; try { json = JSON.stringify(aThing); } catch (ex) { // Can't use a real ellipsis here, because cmd.exe isn't unicode-enabled json = "{" + Object.keys(aThing).join(":..,") + ":.., " + "}"; } return type + fmt(json, 50, 0); } if (typeof aThing == "function") { return fmt(aThing.toString().replace(/\s+/g, " "), 80, 0); } var str = aThing.toString().replace(/\n/g, "|"); return fmt(str, 80, 0); } /** * Create a simple debug representation of a given element. * * @param {nsIDOMElement} aElement * The element to debug * @return {string} * A simple single line representation of aElement */ function debugElement(aElement) { return "<" + aElement.tagName + (aElement.id ? "#" + aElement.id : "") + (aElement.className ? "." + aElement.className.split(" ").join(" .") : "") + ">"; } /** * A multi line stringification of an object, designed for use by humans * * @param {any} aThing * The object to be stringified * @return {string} * A multi line representation of aThing */ function log(aThing) { if (aThing === null) { return "null\n"; } if (aThing === undefined) { return "undefined\n"; } if (typeof aThing == "object") { var reply = ""; var type = getCtorName(aThing); if (type == "Error") { reply += " " + aThing.message + "\n"; reply += logProperty("stack", aThing.stack); } else if (aThing instanceof Node && aThing.tagName) { reply += " " + debugElement(aThing) + "\n"; } else { var keys = Object.getOwnPropertyNames(aThing); if (keys.length > 0) { reply += type + "\n"; keys.forEach(function(aProp) { reply += logProperty(aProp, aThing[aProp]); }, this); } else { reply += type + "\n"; var root = aThing; var logged = []; while (root != null) { var properties = Object.keys(root); properties.sort(); properties.forEach(function(property) { if (!(property in logged)) { logged[property] = property; reply += logProperty(property, aThing[property]); } }); root = Object.getPrototypeOf(root); if (root != null) { reply += ' - prototype ' + getCtorName(root) + '\n'; } } } } return reply; } return " " + aThing.toString() + "\n"; } /** * Helper for log() which converts a property/value pair into an output * string * * @param {string} aProp * The name of the property to include in the output string * @param {object} aValue * Value assigned to aProp to be converted to a single line string * @return {string} * Multi line output string describing the property/value pair */ function logProperty(aProp, aValue) { var reply = ""; if (aProp == "stack" && typeof value == "string") { var trace = parseStack(aValue); reply += formatTrace(trace); } else { reply += " - " + aProp + " = " + stringify(aValue) + "\n"; } return reply; } /** * Parse a stack trace, returning an array of stack frame objects, where * each has file/line/call members * * @param {string} aStack * The serialized stack trace * @return {object[]} * Array of { file: "...", line: NNN, call: "..." } objects */ function parseStack(aStack) { var trace = []; aStack.split("\n").forEach(function(line) { if (!line) { return; } var at = line.lastIndexOf("@"); var posn = line.substring(at + 1); trace.push({ file: posn.split(":")[0], line: posn.split(":")[1], call: line.substring(0, at) }); }, this); return trace; } /** * parseStack() takes output from an exception from which it creates the an * array of stack frame objects, this has the same output but using data from * Components.stack * * @param {string} aFrame * The stack frame from which to begin the walk * @return {object[]} * Array of { file: "...", line: NNN, call: "..." } objects */ function getStack(aFrame) { if (!aFrame) { aFrame = Components.stack.caller; } var trace = []; while (aFrame) { trace.push({ file: aFrame.filename, line: aFrame.lineNumber, call: aFrame.name }); aFrame = aFrame.caller; } return trace; } /** * Take the output from parseStack() and convert it to nice readable * output * * @param {object[]} aTrace * Array of trace objects as created by parseStack() * @return {string} Multi line report of the stack trace */ function formatTrace(aTrace) { var reply = ""; aTrace.forEach(function(frame) { reply += fmt(frame.file, 20, 20, { truncate: "start" }) + " " + fmt(frame.line, 5, 5) + " " + fmt(frame.call, 75, 75) + "\n"; }); return reply; } /** * Create a function which will output a concise level of output when used * as a logging function * * @param {string} aLevel * A prefix to all output generated from this function detailing the * level at which output occurred * @return {function} * A logging function * @see createMultiLineDumper() */ function createDumper(aLevel) { return function() { var args = Array.prototype.slice.call(arguments, 0); var data = args.map(function(arg) { return stringify(arg); }); dump(aLevel + ": " + data.join(", ") + "\n"); }; } /** * Create a function which will output more detailed level of output when * used as a logging function * * @param {string} aLevel * A prefix to all output generated from this function detailing the * level at which output occurred * @return {function} * A logging function * @see createDumper() */ function createMultiLineDumper(aLevel) { return function() { dump(aLevel + "\n"); var args = Array.prototype.slice.call(arguments, 0); args.forEach(function(arg) { dump(log(arg)); }); }; } /** * Build out the console object */ console.debug = createMultiLineDumper("debug"); console.log = createDumper("log"); console.info = createDumper("info"); console.warn = createDumper("warn"); console.error = createMultiLineDumper("error"); console.trace = function Console_trace() { var trace = getStack(Components.stack.caller); dump(formatTrace(trace) + "\n"); }, console.clear = function Console_clear() {}; console.dir = createMultiLineDumper("dir"); console.dirxml = createMultiLineDumper("dirxml"); console.group = createDumper("group"); console.groupEnd = createDumper("groupEnd"); })(); /* * Copyright 2009-2011 Mozilla Foundation and contributors * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ /** * Define a module along with a payload. * @param moduleName Name for the payload * @param deps Ignored. For compatibility with CommonJS AMD Spec * @param payload Function with (require, exports, module) params */ function define(moduleName, deps, payload) { if (typeof moduleName != "string") { console.error(this.depth + " Error: Module name is not a string."); console.trace(); return; } if (arguments.length == 2) { payload = deps; } if (define.debugDependencies) { console.log("define: " + moduleName + " -> " + payload.toString() .slice(0, 40).replace(/\n/, '\\n').replace(/\r/, '\\r') + "..."); } if (moduleName in define.modules) { console.error(this.depth + " Error: Redefining module: " + moduleName); } define.modules[moduleName] = payload; } /** * The global store of un-instantiated modules */ define.modules = {}; /** * Should we console.log on module definition/instantiation/requirement? */ define.debugDependencies = false; /** * Self executing function in which Domain is defined, and attached to define */ (function() { /** * We invoke require() in the context of a Domain so we can have multiple * sets of modules running separate from each other. * This contrasts with JSMs which are singletons, Domains allows us to * optionally load a CommonJS module twice with separate data each time. * Perhaps you want 2 command lines with a different set of commands in each, * for example. */ function Domain() { this.modules = {}; if (define.debugDependencies) { this.depth = ""; } } /** * Lookup module names and resolve them by calling the definition function if * needed. * There are 2 ways to call this, either with an array of dependencies and a * callback to call when the dependencies are found (which can happen * asynchronously in an in-page context) or with a single string an no * callback where the dependency is resolved synchronously and returned. * The API is designed to be compatible with the CommonJS AMD spec and * RequireJS. * @param deps A name, or array of names for the payload * @param callback Function to call when the dependencies are resolved * @return The module required or undefined for array/callback method */ Domain.prototype.require = function(deps, callback) { if (Array.isArray(deps)) { var params = deps.map(function(dep) { return this.lookup(dep); }, this); if (callback) { callback.apply(null, params); } return undefined; } else { return this.lookup(deps); } }; /** * Lookup module names and resolve them by calling the definition function if * needed. * @param moduleName A name for the payload to lookup * @return The module specified by aModuleName or null if not found */ Domain.prototype.lookup = function(moduleName) { if (moduleName in this.modules) { var module = this.modules[moduleName]; if (define.debugDependencies) { console.log(this.depth + " Using module: " + moduleName); } return module; } if (!(moduleName in define.modules)) { console.error(this.depth + " Missing module: " + moduleName); return null; } var module = define.modules[moduleName]; if (define.debugDependencies) { console.log(this.depth + " Compiling module: " + moduleName); } if (typeof module == "function") { if (define.debugDependencies) { this.depth += "."; } var exports = {}; try { module(this.require.bind(this), exports, { id: moduleName, uri: "" }); } catch (ex) { console.error("Error using module: " + moduleName, ex); throw ex; } module = exports; if (define.debugDependencies) { this.depth = this.depth.slice(0, -1); } } // cache the resulting module object for next time this.modules[moduleName] = module; return module; }; /** * Expose the Domain constructor and a global domain (on the define function * to avoid exporting more than we need. This is a common pattern with * require systems) */ define.Domain = Domain; define.globalDomain = new Domain(); })(); /** * Expose a default require function which is the require of the global * sandbox to make it easy to use. */ var require = define.globalDomain.require.bind(define.globalDomain); /* * Copyright 2009-2011 Mozilla Foundation and contributors * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ var mozl10n = {}; (function(aMozl10n) { var temp = {}; Components.utils.import("resource://gre/modules/Services.jsm", temp); var stringBundle = temp.Services.strings.createBundle( "chrome://browser/locale/devtools/gclicommands.properties"); /** * Lookup a string in the GCLI string bundle * @param name The name to lookup * @return The looked up name */ aMozl10n.lookup = function(name) { try { return stringBundle.GetStringFromName(name); } catch (ex) { throw new Error("Failure in lookup('" + name + "')"); } }; /** * Lookup a string in the GCLI string bundle * @param name The name to lookup * @param swaps An array of swaps. See stringBundle.formatStringFromName * @return The looked up name */ aMozl10n.lookupFormat = function(name, swaps) { try { return stringBundle.formatStringFromName(name, swaps, swaps.length); } catch (ex) { throw new Error("Failure in lookupFormat('" + name + "')"); } }; })(mozl10n); define('gcli/index', ['require', 'exports', 'module' , 'gcli/canon', 'gcli/types/basic', 'gcli/types/command', 'gcli/types/javascript', 'gcli/types/node', 'gcli/types/resource', 'gcli/types/setting', 'gcli/types/selection', 'gcli/settings', 'gcli/ui/intro', 'gcli/ui/focus', 'gcli/ui/fields/basic', 'gcli/ui/fields/javascript', 'gcli/ui/fields/selection', 'gcli/commands/help', 'gcli/ui/ffdisplay'], function(require, exports, module) { // The API for use by command authors exports.addCommand = require('gcli/canon').addCommand; exports.removeCommand = require('gcli/canon').removeCommand; exports.lookup = mozl10n.lookup; exports.lookupFormat = mozl10n.lookupFormat; // Internal startup process. Not exported require('gcli/types/basic').startup(); require('gcli/types/command').startup(); require('gcli/types/javascript').startup(); require('gcli/types/node').startup(); require('gcli/types/resource').startup(); require('gcli/types/setting').startup(); require('gcli/types/selection').startup(); require('gcli/settings').startup(); require('gcli/ui/intro').startup(); require('gcli/ui/focus').startup(); require('gcli/ui/fields/basic').startup(); require('gcli/ui/fields/javascript').startup(); require('gcli/ui/fields/selection').startup(); require('gcli/commands/help').startup(); // Some commands require customizing for Firefox before we include them // require('gcli/cli').startup(); // require('gcli/commands/pref').startup(); var FFDisplay = require('gcli/ui/ffdisplay').FFDisplay; /** * API for use by HUDService only. * This code is internal and subject to change without notice. */ exports._internal = { require: require, define: define, console: console, /** * createView() for Firefox requires an options object with the following * members: * - contentDocument: From the window of the attached tab * - chromeDocument: GCLITerm.document * - environment.hudId: GCLITerm.hudId * - jsEnvironment.globalObject: 'window' * - jsEnvironment.evalFunction: 'eval' in a sandbox * - inputElement: GCLITerm.inputNode * - completeElement: GCLITerm.completeNode * - hintElement: GCLITerm.hintNode * - inputBackgroundElement: GCLITerm.inputStack */ createDisplay: function(opts) { return new FFDisplay(opts); } }; }); /* * Copyright 2009-2011 Mozilla Foundation and contributors * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ define('gcli/canon', ['require', 'exports', 'module' , 'gcli/util', 'gcli/l10n', 'gcli/types', 'gcli/types/basic'], function(require, exports, module) { var canon = exports; var util = require('gcli/util'); var l10n = require('gcli/l10n'); var types = require('gcli/types'); var Status = require('gcli/types').Status; var BooleanType = require('gcli/types/basic').BooleanType; /** * Implement the localization algorithm for any documentation objects (i.e. * description and manual) in a command. * @param data The data assigned to a description or manual property * @param onUndefined If data == null, should we return the data untouched or * lookup a 'we don't know' key in it's place. */ function lookup(data, onUndefined) { if (data == null) { if (onUndefined) { return l10n.lookup(onUndefined); } return data; } if (typeof data === 'string') { return data; } if (typeof data === 'object') { if (data.key) { return l10n.lookup(data.key); } var locales = l10n.getPreferredLocales(); var translated; locales.some(function(locale) { translated = data[locale]; return translated != null; }); if (translated != null) { return translated; } console.error('Can\'t find locale in descriptions: ' + 'locales=' + JSON.stringify(locales) + ', ' + 'description=' + JSON.stringify(data)); return '(No description)'; } return l10n.lookup(onUndefined); } /** * The command object is mostly just setup around a commandSpec (as passed to * #addCommand()). */ function Command(commandSpec) { Object.keys(commandSpec).forEach(function(key) { this[key] = commandSpec[key]; }, this); if (!this.name) { throw new Error('All registered commands must have a name'); } if (this.params == null) { this.params = []; } if (!Array.isArray(this.params)) { throw new Error('command.params must be an array in ' + this.name); } this.description = 'description' in this ? this.description : undefined; this.description = lookup(this.description, 'canonDescNone'); this.manual = 'manual' in this ? this.manual : undefined; this.manual = lookup(this.manual); // At this point this.params has nested param groups. We want to flatten it // out and replace the param object literals with Parameter objects var paramSpecs = this.params; this.params = []; // Track if the user is trying to mix default params and param groups. // All the non-grouped parameters must come before all the param groups // because non-grouped parameters can be assigned positionally, so their // index is important. We don't want 'holes' in the order caused by // parameter groups. var usingGroups = false; if (this.returnType == null) { this.returnType = 'string'; } // In theory this could easily be made recursive, so param groups could // contain nested param groups. Current thinking is that the added // complexity for the UI probably isn't worth it, so this implementation // prevents nesting. paramSpecs.forEach(function(spec) { if (!spec.group) { if (usingGroups) { console.error('Parameters can\'t come after param groups.' + ' Ignoring ' + this.name + '/' + spec.name); } else { var param = new Parameter(spec, this, null); this.params.push(param); } } else { spec.params.forEach(function(ispec) { var param = new Parameter(ispec, this, spec.group); this.params.push(param); }, this); usingGroups = true; } }, this); } canon.Command = Command; /** * A wrapper for a paramSpec so we can sort out shortened versions names for * option switches */ function Parameter(paramSpec, command, groupName) { this.command = command || { name: 'unnamed' }; this.paramSpec = paramSpec; this.name = this.paramSpec.name; this.type = this.paramSpec.type; this.groupName = groupName; this.defaultValue = this.paramSpec.defaultValue; if (!this.name) { throw new Error('In ' + this.command.name + ': all params must have a name'); } var typeSpec = this.type; this.type = types.getType(typeSpec); if (this.type == null) { console.error('Known types: ' + types.getTypeNames().join(', ')); throw new Error('In ' + this.command.name + '/' + this.name + ': can\'t find type for: ' + JSON.stringify(typeSpec)); } // boolean parameters have an implicit defaultValue:false, which should // not be changed. See the docs. if (this.type instanceof BooleanType) { if (this.defaultValue !== undefined) { console.error('In ' + this.command.name + '/' + this.name + ': boolean parameters can not have a defaultValue.' + ' Ignoring'); } this.defaultValue = false; } // Check the defaultValue for validity. // Both undefined and null get a pass on this test. undefined is used when // there is no defaultValue, and null is used when the parameter is // optional, neither are required to parse and stringify. if (this.defaultValue != null) { try { var defaultText = this.type.stringify(this.defaultValue); var defaultConversion = this.type.parseString(defaultText); if (defaultConversion.getStatus() !== Status.VALID) { console.error('In ' + this.command.name + '/' + this.name + ': Error round tripping defaultValue. status = ' + defaultConversion.getStatus()); } } catch (ex) { console.error('In ' + this.command.name + '/' + this.name + ': ' + ex); } } // Some typed (boolean, array) have a non 'undefined' blank value. Give the // type a chance to override the default defaultValue of undefined if (this.defaultValue === undefined) { this.defaultValue = this.type.getBlank().value; } } /** * Does the given name uniquely identify this param (among the other params * in this command) * @param name The name to check */ Parameter.prototype.isKnownAs = function(name) { if (name === '--' + this.name) { return true; } return false; }; /** * Read the default value for this parameter either from the parameter itself * (if this function has been over-ridden) or from the type, or from calling * parseString on an empty string */ Parameter.prototype.getBlank = function() { var conversion; if (this.type.getBlank) { return this.type.getBlank(); } return this.type.parseString(''); }; /** * Resolve the manual for this parameter, by looking in the paramSpec * and doing a l10n lookup */ Object.defineProperty(Parameter.prototype, 'manual', { get: function() { return lookup(this.paramSpec.manual || undefined); }, enumerable: true }); /** * Resolve the description for this parameter, by looking in the paramSpec * and doing a l10n lookup */ Object.defineProperty(Parameter.prototype, 'description', { get: function() { return lookup(this.paramSpec.description || undefined, 'canonDescNone'); }, enumerable: true }); /** * Is the user required to enter data for this parameter? (i.e. has * defaultValue been set to something other than undefined) */ Object.defineProperty(Parameter.prototype, 'isDataRequired', { get: function() { return this.defaultValue === undefined; }, enumerable: true }); /** * Are we allowed to assign data to this parameter using positional * parameters? */ Object.defineProperty(Parameter.prototype, 'isPositionalAllowed', { get: function() { return this.groupName == null; }, enumerable: true }); canon.Parameter = Parameter; /** * A lookup hash of our registered commands */ var commands = {}; /** * A sorted list of command names, we regularly want them in order, so pre-sort */ var commandNames = []; /** * A lookup of the original commandSpecs by command name */ var commandSpecs = {}; /** * Add a command to the canon of known commands. * This function is exposed to the outside world (via gcli/index). It is * documented in docs/index.md for all the world to see. * @param commandSpec The command and its metadata. * @return The new command */ canon.addCommand = function addCommand(commandSpec) { if (commands[commandSpec.name] != null) { // Roughly canon.removeCommand() without the event call, which we do later delete commands[commandSpec.name]; commandNames = commandNames.filter(function(test) { return test !== commandSpec.name; }); } var command = new Command(commandSpec); commands[commandSpec.name] = command; commandNames.push(commandSpec.name); commandNames.sort(); commandSpecs[commandSpec.name] = commandSpec; canon.onCanonChange(); return command; }; /** * Remove an individual command. The opposite of #addCommand(). * @param commandOrName Either a command name or the command itself. */ canon.removeCommand = function removeCommand(commandOrName) { var name = typeof commandOrName === 'string' ? commandOrName : commandOrName.name; // See start of canon.addCommand if changing this code delete commands[name]; delete commandSpecs[name]; commandNames = commandNames.filter(function(test) { return test !== name; }); canon.onCanonChange(); }; /** * Retrieve a command by name * @param name The name of the command to retrieve */ canon.getCommand = function getCommand(name) { // '|| undefined' is to silence 'reference to undefined property' warnings return commands[name] || undefined; }; /** * Get an array of all the registered commands. */ canon.getCommands = function getCommands() { // return Object.values(commands); return Object.keys(commands).map(function(name) { return commands[name]; }, this); }; /** * Get an array containing the names of the registered commands. */ canon.getCommandNames = function getCommandNames() { return commandNames.slice(0); }; /** * Get access to the stored commandMetaDatas (i.e. before they were made into * instances of Command/Parameters) so we can remote them. */ canon.getCommandSpecs = function getCommandSpecs() { return commandSpecs; }; /** * Enable people to be notified of changes to the list of commands */ canon.onCanonChange = util.createEvent('canon.onCanonChange'); /** * CommandOutputManager stores the output objects generated by executed * commands. * * CommandOutputManager is exposed (via canon.commandOutputManager) to the the * outside world and could (but shouldn't) be used before gcli.startup() has * been called. This could should be defensive to that where possible, and we * should certainly document if the use of it or similar will fail if used too * soon. */ function CommandOutputManager() { this.onOutput = util.createEvent('CommandOutputManager.onOutput'); } canon.CommandOutputManager = CommandOutputManager; /** * We maintain a global command output manager for the majority case where * there is only one important set of outputs. */ canon.commandOutputManager = new CommandOutputManager(); }); /* * Copyright 2009-2011 Mozilla Foundation and contributors * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ define('gcli/util', ['require', 'exports', 'module' ], function(require, exports, module) { /* * A number of DOM manipulation and event handling utilities. */ //------------------------------------------------------------------------------ var eventDebug = false; /** * Useful way to create a name for a handler, used in createEvent() */ function nameFunction(handler) { var scope = handler.scope ? handler.scope.constructor.name + '.' : ''; var name = handler.func.name; if (name) { return scope + name; } for (var prop in handler.scope) { if (handler.scope[prop] === handler.func) { return scope + prop; } } return scope + handler.func; } /** * Create an event. * For use as follows: * * function Hat() { * this.putOn = createEvent('Hat.putOn'); * ... * } * Hat.prototype.adorn = function(person) { * this.putOn({ hat: hat, person: person }); * ... * } * * var hat = new Hat(); * hat.putOn.add(function(ev) { * console.log('The hat ', ev.hat, ' has is worn by ', ev.person); * }, scope); * * @param name Optional name to help with debugging */ exports.createEvent = function(name) { var handlers = []; var holdFire = false; var heldEvents = []; var eventCombiner = undefined; /** * This is how the event is triggered. * @param ev The event object to be passed to the event listeners */ var event = function(ev) { if (holdFire) { heldEvents.push(ev); if (eventDebug) { console.log('Held fire: ' + name, ev); } return; } if (eventDebug) { console.group('Fire: ' + name + ' to ' + handlers.length + ' listeners', ev); } // Use for rather than forEach because it step debugs better, which is // important for debugging events for (var i = 0; i < handlers.length; i++) { var handler = handlers[i]; if (eventDebug) { console.log(nameFunction(handler)); } handler.func.call(handler.scope, ev); } if (eventDebug) { console.groupEnd(); } }; /** * Add a new handler function * @param func The function to call when this event is triggered * @param scope Optional 'this' object for the function call */ event.add = function(func, scope) { handlers.push({ func: func, scope: scope }); }; /** * Remove a handler function added through add(). Both func and scope must * be strict equals (===) the values used in the call to add() * @param func The function to call when this event is triggered * @param scope Optional 'this' object for the function call */ event.remove = function(func, scope) { var found = false; handlers = handlers.filter(function(test) { var noMatch = (test.func !== func && test.scope !== scope); if (!noMatch) { found = true; } return noMatch; }); if (!found) { console.warn('Failed to remove handler from ' + name); } }; /** * Remove all handlers. * Reset the state of this event back to it's post create state */ event.removeAll = function() { handlers = []; }; /** * Temporarily prevent this event from firing. * @see resumeFire(ev) */ event.holdFire = function() { if (eventDebug) { console.group('Holding fire: ' + name); } holdFire = true; }; /** * Resume firing events. * If there are heldEvents, then we fire one event to cover them all. If an * event combining function has been provided then we use that to combine the * events. Otherwise the last held event is used. * @see holdFire() */ event.resumeFire = function() { if (eventDebug) { console.groupEnd('Resume fire: ' + name); } if (holdFire !== true) { throw new Error('Event not held: ' + name); } holdFire = false; if (heldEvents.length === 0) { return; } if (heldEvents.length === 1) { event(heldEvents[0]); } else { var first = heldEvents[0]; var last = heldEvents[heldEvents.length - 1]; if (eventCombiner) { event(eventCombiner(first, last, heldEvents)); } else { event(last); } } heldEvents = []; }; /** * When resumeFire has a number of events to combine, by default it just * picks the last, however you can provide an eventCombiner which returns a * combined event. * eventCombiners will be passed 3 parameters: * - first The first event to be held * - last The last event to be held * - all An array containing all the held events * The return value from an eventCombiner is expected to be an event object */ Object.defineProperty(event, 'eventCombiner', { set: function(newEventCombiner) { if (typeof newEventCombiner !== 'function') { throw new Error('eventCombiner is not a function'); } eventCombiner = newEventCombiner; }, enumerable: true }); return event; }; //------------------------------------------------------------------------------ /** * XHTML namespace */ exports.NS_XHTML = 'http://www.w3.org/1999/xhtml'; /** * XUL namespace */ exports.NS_XUL = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'; /** * Create an HTML or XHTML element depending on whether the document is HTML * or XML based. Where HTML/XHTML elements are distinguished by whether they * are created using doc.createElementNS('http://www.w3.org/1999/xhtml', tag) * or doc.createElement(tag) * If you want to create a XUL element then you don't have a problem knowing * what namespace you want. * @param doc The document in which to create the element * @param tag The name of the tag to create * @returns The created element */ exports.createElement = function(doc, tag) { if (exports.isXmlDocument(doc)) { return doc.createElementNS(exports.NS_XHTML, tag); } else { return doc.createElement(tag); } }; /** * Remove all the child nodes from this node * @param elem The element that should have it's children removed */ exports.clearElement = function(elem) { while (elem.hasChildNodes()) { elem.removeChild(elem.firstChild); } }; var isAllWhitespace = /^\s*$/; /** * Iterate over the children of a node looking for TextNodes that have only * whitespace content and remove them. * This utility is helpful when you have a template which contains whitespace * so it looks nice, but where the whitespace interferes with the rendering of * the page * @param elem The element which should have blank whitespace trimmed * @param deep Should this node removal include child elements */ exports.removeWhitespace = function(elem, deep) { var i = 0; while (i < elem.childNodes.length) { var child = elem.childNodes.item(i); if (child.nodeType === 3 /*Node.TEXT_NODE*/ && isAllWhitespace.test(child.textContent)) { elem.removeChild(child); } else { if (deep && child.nodeType === 1 /*Node.ELEMENT_NODE*/) { exports.removeWhitespace(child, deep); } i++; } } }; /** * Create a style element in the document head, and add the given CSS text to * it. * @param cssText The CSS declarations to append * @param doc The document element to work from * @param id Optional id to assign to the created style tag. If the id already * exists on the document, we do not add the CSS again. */ exports.importCss = function(cssText, doc, id) { if (!cssText) { return undefined; } doc = doc || document; if (!id) { id = 'hash-' + hash(cssText); } var found = doc.getElementById(id); if (found) { if (found.tagName.toLowerCase() !== 'style') { console.error('Warning: importCss passed id=' + id + ', but that pre-exists (and isn\'t a style tag)'); } return found; } var style = exports.createElement(doc, 'style'); style.id = id; style.appendChild(doc.createTextNode(cssText)); var head = doc.getElementsByTagName('head')[0] || doc.documentElement; head.appendChild(style); return style; }; /** * Simple hash function which happens to match Java's |String.hashCode()| * Done like this because I we don't need crypto-security, but do need speed, * and I don't want to spend a long time working on it. * @see http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/ */ function hash(str) { var hash = 0; if (str.length == 0) { return hash; } for (var i = 0; i < str.length; i++) { var char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return hash; } /** * There are problems with innerHTML on XML documents, so we need to do a dance * using document.createRange().createContextualFragment() when in XML mode */ exports.setContents = function(elem, contents) { if (typeof HTMLElement !== 'undefined' && contents instanceof HTMLElement) { exports.clearElement(elem); elem.appendChild(contents); return; } if (exports.isXmlDocument(elem.ownerDocument)) { try { var ns = elem.ownerDocument.documentElement.namespaceURI; if (!ns) { ns = exports.NS_XHTML; } exports.clearElement(elem); contents = '
We also record validity information where applicable. *
For values, null and undefined have distinct definitions. null means * that a value has been provided, undefined means that it has not. * Thus, null is a valid default value, and common because it identifies an * parameter that is optional. undefined means there is no value from * the command line. * *
Requisition publishes the following events: *
* The returned object has the following members:
* The Argument objects are as output from #_tokenize() rather than as applied * to Assignments by #_assign() (i.e. they are not instances of NamedArgument, * ArrayArgument, etc). *
* To get at the arguments applied to the assignments simply call * arg.assignment.arg. If arg.assignment.arg !== arg then * the arg applied to the assignment will contain the original arg. * See #_assign() for details. */ Requisition.prototype.createInputArgTrace = function() { if (!this._args) { throw new Error('createInputMap requires a command line. See source.'); // If this is a problem then we can fake command line input using // something like the code in #toCanonicalString(). } var args = []; var i; this._args.forEach(function(arg) { for (i = 0; i < arg.prefix.length; i++) { args.push({ arg: arg, char: arg.prefix[i], part: 'prefix' }); } for (i = 0; i < arg.text.length; i++) { args.push({ arg: arg, char: arg.text[i], part: 'text' }); } for (i = 0; i < arg.suffix.length; i++) { args.push({ arg: arg, char: arg.suffix[i], part: 'suffix' }); } }); return args; }; /** * Reconstitute the input from the args */ Requisition.prototype.toString = function() { if (this._args) { return this._args.map(function(arg) { return arg.toString(); }).join(''); } return this.toCanonicalString(); }; /** * If the last character is whitespace then things that we suggest to add to * the end don't need a space prefix. * While this is quite a niche function, it has 2 benefits: * - it's more correct because we can distinguish between final whitespace that * is part of an unclosed string, and parameter separating whitespace. * - also it's faster than toString() the whole thing and checking the end char * @return true iff the last character is interpreted as parameter separating * whitespace */ Requisition.prototype.typedEndsWithWhitespace = function() { if (this._args) { return this._args.slice(-1)[0].suffix.slice(-1) === ' '; } return this.toCanonicalString().slice(-1) === ' '; }; /** * Return an array of Status scores so we can create a marked up * version of the command line input. * @param cursor We only take a status of INCOMPLETE to be INCOMPLETE when the * cursor is actually in the argument. Otherwise it's an error. * @return Array of objects each containing status property and a * string property containing the characters to which the status * applies. Concatenating the strings in order gives the original input. */ Requisition.prototype.getInputStatusMarkup = function(cursor) { var argTraces = this.createInputArgTrace(); // Generally the 'argument at the cursor' is the argument before the cursor // unless it is before the first char, in which case we take the first. cursor = cursor === 0 ? 0 : cursor - 1; var cTrace = argTraces[cursor]; var markup = []; for (var i = 0; i < argTraces.length; i++) { var argTrace = argTraces[i]; var arg = argTrace.arg; var status = Status.VALID; if (argTrace.part === 'text') { status = arg.assignment.getStatus(arg); // Promote INCOMPLETE to ERROR ... if (status === Status.INCOMPLETE) { // If the cursor is not in a position to be able to complete it if (arg !== cTrace.arg || cTrace.part !== 'text') { // And if we're not in the command if (!(arg.assignment instanceof CommandAssignment)) { status = Status.ERROR; } } } } markup.push({ status: status, string: argTrace.char }); } // De-dupe: merge entries where 2 adjacent have same status var i = 0; while (i < markup.length - 1) { if (markup[i].status === markup[i + 1].status) { markup[i].string += markup[i + 1].string; markup.splice(i + 1, 1); } else { i++; } } return markup; }; /** * Look through the arguments attached to our assignments for the assignment * at the given position. * @param {number} cursor The cursor position to query */ Requisition.prototype.getAssignmentAt = function(cursor) { if (!this._args) { console.trace(); throw new Error('Missing args'); } // We short circuit this one because we may have no args, or no args with // any size and the alg below only finds arguments with size. if (cursor === 0) { return this.commandAssignment; } var assignForPos = []; var i, j; for (i = 0; i < this._args.length; i++) { var arg = this._args[i]; var assignment = arg.assignment; // prefix and text are clearly part of the argument for (j = 0; j < arg.prefix.length; j++) { assignForPos.push(assignment); } for (j = 0; j < arg.text.length; j++) { assignForPos.push(assignment); } // suffix looks forwards if (this._args.length > i + 1) { // first to the next argument assignment = this._args[i + 1].assignment; } else if (assignment && assignment.paramIndex + 1 < this.assignmentCount) { // then to the next assignment assignment = this.getAssignment(assignment.paramIndex + 1); } for (j = 0; j < arg.suffix.length; j++) { assignForPos.push(assignment); } } // Possible shortcut, we don't really need to go through all the args // to work out the solution to this var reply = assignForPos[cursor - 1]; if (!reply) { throw new Error('Missing assignment.' + ' cursor=' + cursor + ' text.length=' + this.toString().length); } return reply; }; /** * Entry point for keyboard accelerators or anything else that wants to execute * a command. There are 3 ways to call exec(): * 1. Without any parameters. This assumes that the command to be executed has * already been parsed by the requisition using update(). * 2. With a string parameter, or an object with a 'typed' property. This is * effectively a shortcut for calling update(typed); exec(); * 3. With input having a 'command' property which is either a command object * (i.e. from canon.getCommand) or a string which can be passed to * canon.getCommand() plus and optional 'args' property which contains the * argument values as passed to command.exec. This method is significantly * faster, and designed for use from keyboard shortcuts. * In addition to these properties, the input parameter can contain a 'hidden' * property which can be set to true to hide the output from the * CommandOutputManager. * @param input (optional) The command to execute. See above. */ Requisition.prototype.exec = function(input) { var command; var args; var hidden = false; if (input && input.hidden) { hidden = true; } if (input) { if (typeof input === 'string') { this.update(input); } else if (typeof input.typed === 'string') { this.update(input.typed); } else if (input.command != null) { // Fast track by looking up the command directly since passed args // means there is no command line to parse. command = canon.getCommand(input.command); if (!command) { console.error('Command not found: ' + input.command); } args = input.args; } } if (!command) { command = this.commandAssignment.value; args = this.getArgsObject(); } if (!command) { throw new Error('Unknown command'); } // Display JavaScript input without the initial { or closing } var typed = this.toString(); if (evalCommandSpec.evalRegexp.test(typed)) { typed = typed.replace(evalCommandSpec.evalRegexp, ''); // Bug 717763: What if the JavaScript naturally ends with a }? typed = typed.replace(/\s*}\s*$/, ''); } var output = new Output({ command: command, args: args, typed: typed, canonical: this.toCanonicalString(), hidden: hidden }); this.commandOutputManager.onOutput({ output: output }); try { var context = exports.createExecutionContext(this); var reply = command.exec(args, context); if (reply != null && reply.isPromise) { reply.then( function(data) { output.complete(data); }, function(error) { output.error = true; output.complete(error); }); output.promise = reply; // Add progress to our promise and add a handler for it here // See bug 659300 } else { output.complete(reply); } } catch (ex) { console.error(ex); output.error = true; output.complete(ex); } this.update(''); return output; }; /** * Called by the UI when ever the user interacts with a command line input * @param typed The contents of the input field *
The general sequence is: *
\n" + " GCLI is an experiment to create a highly usable graphical command\n" + " line for developers. It's not a JavaScript\n" + " REPL, so\n" + " it focuses on speed of input over JavaScript syntax and a rich display over\n" + " monospace output.
\n" + "\n" + "Type help for a list of commands,\n" +
" or press F1/Escape
to show/hide command hints.