/* * 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 = '
' + contents + '
'; var range = elem.ownerDocument.createRange(); var child = range.createContextualFragment(contents).firstChild; while (child.hasChildNodes()) { elem.appendChild(child.firstChild); } } catch (ex) { console.error('Bad XHTML', ex); console.trace(); throw ex; } } else { elem.innerHTML = contents; } }; /** * Load some HTML into the given document and return a DOM element. * This utility assumes that the html has a single root (other than whitespace) */ exports.toDom = function(document, html) { var div = exports.createElement(document, 'div'); exports.setContents(div, html); return div.children[0]; }; /** * How to detect if we're in an XML document. * In a Mozilla we check that document.xmlVersion = null, however in Chrome * we use document.contentType = undefined. * @param doc The document element to work from (defaulted to the global * 'document' if missing */ exports.isXmlDocument = function(doc) { doc = doc || document; // Best test for Firefox if (doc.contentType && doc.contentType != 'text/html') { return true; } // Best test for Chrome if (doc.xmlVersion != null) { return true; } return false; }; /** * Find the position of [element] in [nodeList]. * @returns an index of the match, or -1 if there is no match */ function positionInNodeList(element, nodeList) { for (var i = 0; i < nodeList.length; i++) { if (element === nodeList[i]) { return i; } } return -1; } /** * Find a unique CSS selector for a given element * @returns a string such that ele.ownerDocument.querySelector(reply) === ele * and ele.ownerDocument.querySelectorAll(reply).length === 1 */ exports.findCssSelector = function(ele) { var document = ele.ownerDocument; if (ele.id && document.getElementById(ele.id) === ele) { return '#' + ele.id; } // Inherently unique by tag name var tagName = ele.tagName.toLowerCase(); if (tagName === 'html') { return 'html'; } if (tagName === 'head') { return 'head'; } if (tagName === 'body') { return 'body'; } if (ele.parentNode == null) { console.log('danger: ' + tagName); } // We might be able to find a unique class name var selector, index, matches; if (ele.classList.length > 0) { for (var i = 0; i < ele.classList.length; i++) { // Is this className unique by itself? selector = '.' + ele.classList.item(i); matches = document.querySelectorAll(selector); if (matches.length === 1) { return selector; } // Maybe it's unique with a tag name? selector = tagName + selector; matches = document.querySelectorAll(selector); if (matches.length === 1) { return selector; } // Maybe it's unique using a tag name and nth-child index = positionInNodeList(ele, ele.parentNode.children) + 1; selector = selector + ':nth-child(' + index + ')'; matches = document.querySelectorAll(selector); if (matches.length === 1) { return selector; } } } // So we can be unique w.r.t. our parent, and use recursion index = positionInNodeList(ele, ele.parentNode.children) + 1; selector = exports.findCssSelector(ele.parentNode) + ' > ' + tagName + ':nth-child(' + index + ')'; return selector; }; /** * Work out the path for images. */ exports.createUrlLookup = function(callingModule) { return function imageUrl(path) { try { return require('text!gcli/ui/' + path); } catch (ex) { // Under node/unamd callingModule is provided by node. This code isn't // the right answer but it's enough to pass all the unit tests and get // test coverage information, which is all we actually care about here. if (callingModule.filename) { return callingModule.filename + path; } var filename = callingModule.id.split('/').pop() + '.js'; if (callingModule.uri.substr(-filename.length) !== filename) { console.error('Can\'t work out path from module.uri/module.id'); return path; } if (callingModule.uri) { var end = callingModule.uri.length - filename.length - 1; return callingModule.uri.substr(0, end) + '/' + path; } return filename + '/' + path; } }; }; //------------------------------------------------------------------------------ /** * Keyboard handling is a mess. http://unixpapa.com/js/key.html * It would be good to use DOM L3 Keyboard events, * http://www.w3.org/TR/2010/WD-DOM-Level-3-Events-20100907/#events-keyboardevents * however only Webkit supports them, and there isn't a shim on Monernizr: * https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-browser-Polyfills * and when the code that uses this KeyEvent was written, nothing was clear, * so instead, we're using this unmodern shim: * http://stackoverflow.com/questions/5681146/chrome-10-keyevent-or-something-similar-to-firefoxs-keyevent * See BUG 664991: GCLI's keyboard handling should be updated to use DOM-L3 * https://bugzilla.mozilla.org/show_bug.cgi?id=664991 */ if ('KeyEvent' in this) { exports.KeyEvent = this.KeyEvent; } else { exports.KeyEvent = { DOM_VK_CANCEL: 3, DOM_VK_HELP: 6, DOM_VK_BACK_SPACE: 8, DOM_VK_TAB: 9, DOM_VK_CLEAR: 12, DOM_VK_RETURN: 13, DOM_VK_ENTER: 14, DOM_VK_SHIFT: 16, DOM_VK_CONTROL: 17, DOM_VK_ALT: 18, DOM_VK_PAUSE: 19, DOM_VK_CAPS_LOCK: 20, DOM_VK_ESCAPE: 27, DOM_VK_SPACE: 32, DOM_VK_PAGE_UP: 33, DOM_VK_PAGE_DOWN: 34, DOM_VK_END: 35, DOM_VK_HOME: 36, DOM_VK_LEFT: 37, DOM_VK_UP: 38, DOM_VK_RIGHT: 39, DOM_VK_DOWN: 40, DOM_VK_PRINTSCREEN: 44, DOM_VK_INSERT: 45, DOM_VK_DELETE: 46, DOM_VK_0: 48, DOM_VK_1: 49, DOM_VK_2: 50, DOM_VK_3: 51, DOM_VK_4: 52, DOM_VK_5: 53, DOM_VK_6: 54, DOM_VK_7: 55, DOM_VK_8: 56, DOM_VK_9: 57, DOM_VK_SEMICOLON: 59, DOM_VK_EQUALS: 61, DOM_VK_A: 65, DOM_VK_B: 66, DOM_VK_C: 67, DOM_VK_D: 68, DOM_VK_E: 69, DOM_VK_F: 70, DOM_VK_G: 71, DOM_VK_H: 72, DOM_VK_I: 73, DOM_VK_J: 74, DOM_VK_K: 75, DOM_VK_L: 76, DOM_VK_M: 77, DOM_VK_N: 78, DOM_VK_O: 79, DOM_VK_P: 80, DOM_VK_Q: 81, DOM_VK_R: 82, DOM_VK_S: 83, DOM_VK_T: 84, DOM_VK_U: 85, DOM_VK_V: 86, DOM_VK_W: 87, DOM_VK_X: 88, DOM_VK_Y: 89, DOM_VK_Z: 90, DOM_VK_CONTEXT_MENU: 93, DOM_VK_NUMPAD0: 96, DOM_VK_NUMPAD1: 97, DOM_VK_NUMPAD2: 98, DOM_VK_NUMPAD3: 99, DOM_VK_NUMPAD4: 100, DOM_VK_NUMPAD5: 101, DOM_VK_NUMPAD6: 102, DOM_VK_NUMPAD7: 103, DOM_VK_NUMPAD8: 104, DOM_VK_NUMPAD9: 105, DOM_VK_MULTIPLY: 106, DOM_VK_ADD: 107, DOM_VK_SEPARATOR: 108, DOM_VK_SUBTRACT: 109, DOM_VK_DECIMAL: 110, DOM_VK_DIVIDE: 111, DOM_VK_F1: 112, DOM_VK_F2: 113, DOM_VK_F3: 114, DOM_VK_F4: 115, DOM_VK_F5: 116, DOM_VK_F6: 117, DOM_VK_F7: 118, DOM_VK_F8: 119, DOM_VK_F9: 120, DOM_VK_F10: 121, DOM_VK_F11: 122, DOM_VK_F12: 123, DOM_VK_F13: 124, DOM_VK_F14: 125, DOM_VK_F15: 126, DOM_VK_F16: 127, DOM_VK_F17: 128, DOM_VK_F18: 129, DOM_VK_F19: 130, DOM_VK_F20: 131, DOM_VK_F21: 132, DOM_VK_F22: 133, DOM_VK_F23: 134, DOM_VK_F24: 135, DOM_VK_NUM_LOCK: 144, DOM_VK_SCROLL_LOCK: 145, DOM_VK_COMMA: 188, DOM_VK_PERIOD: 190, DOM_VK_SLASH: 191, DOM_VK_BACK_QUOTE: 192, DOM_VK_OPEN_BRACKET: 219, DOM_VK_BACK_SLASH: 220, DOM_VK_CLOSE_BRACKET: 221, DOM_VK_QUOTE: 222, DOM_VK_META: 224 }; } }); /* * 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/l10n', ['require', 'exports', 'module' ], function(require, exports, module) { Components.utils.import('resource://gre/modules/XPCOMUtils.jsm'); Components.utils.import('resource://gre/modules/Services.jsm'); XPCOMUtils.defineLazyGetter(this, 'stringBundle', function () { return Services.strings.createBundle('chrome://browser/locale/devtools/gcli.properties'); }); /* * Not supported when embedded - we're doing things the Mozilla way not the * require.js way. */ exports.registerStringsSource = function(modulePath) { throw new Error('registerStringsSource is not available in mozilla'); }; exports.unregisterStringsSource = function(modulePath) { throw new Error('unregisterStringsSource is not available in mozilla'); }; exports.lookupSwap = function(key, swaps) { throw new Error('lookupSwap is not available in mozilla'); }; exports.lookupPlural = function(key, ord, swaps) { throw new Error('lookupPlural is not available in mozilla'); }; exports.getPreferredLocales = function() { return [ 'root' ]; }; /** @see lookup() in lib/gcli/l10n.js */ exports.lookup = function(key) { try { // Our memory leak hunter walks reachable objects trying to work out what // type of thing they are using object.constructor.name. If that causes // problems then we can avoid the unknown-key-exception with the following: /* if (key === 'constructor') { return { name: 'l10n-mem-leak-defeat' }; } */ return stringBundle.GetStringFromName(key); } catch (ex) { console.error('Failed to lookup ', key, ex); return key; } }; /** @see propertyLookup in lib/gcli/l10n.js */ exports.propertyLookup = Proxy.create({ get: function(rcvr, name) { return exports.lookup(name); } }); /** @see lookupFormat in lib/gcli/l10n.js */ exports.lookupFormat = function(key, swaps) { try { return stringBundle.formatStringFromName(key, swaps, swaps.length); } catch (ex) { console.error('Failed to format ', key, ex); return key; } }; }); /* * 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/types', ['require', 'exports', 'module' , 'gcli/argument'], function(require, exports, module) { var types = exports; var Argument = require('gcli/argument').Argument; /** * Some types can detect validity, that is to say they can distinguish between * valid and invalid values. * We might want to change these constants to be numbers for better performance */ var Status = { /** * The conversion process worked without any problem, and the value is * valid. There are a number of failure states, so the best way to check * for failure is (x !== Status.VALID) */ VALID: { toString: function() { return 'VALID'; }, valueOf: function() { return 0; } }, /** * A conversion process failed, however it was noted that the string * provided to 'parse()' could be VALID by the addition of more characters, * so the typing may not be actually incorrect yet, just unfinished. * @see Status.ERROR */ INCOMPLETE: { toString: function() { return 'INCOMPLETE'; }, valueOf: function() { return 1; } }, /** * The conversion process did not work, the value should be null and a * reason for failure should have been provided. In addition some * completion values may be available. * @see Status.INCOMPLETE */ ERROR: { toString: function() { return 'ERROR'; }, valueOf: function() { return 2; } }, /** * A combined status is the worser of the provided statuses. The statuses * can be provided either as a set of arguments or a single array */ combine: function() { var combined = Status.VALID; for (var i = 0; i < arguments.length; i++) { var status = arguments[i]; if (Array.isArray(status)) { status = Status.combine.apply(null, status); } if (status > combined) { combined = status; } } return combined; } }; types.Status = Status; /** * The type.parse() method converts an Argument into a value, Conversion is * a wrapper to that value. * Conversion is needed to collect a number of properties related to that * conversion in one place, i.e. to handle errors and provide traceability. * @param value The result of the conversion * @param arg The data from which the conversion was made * @param status See the Status values [VALID|INCOMPLETE|ERROR] defined above. * The default status is Status.VALID. * @param message If status=ERROR, there should be a message to describe the * error. A message is not needed unless for other statuses, but could be * present for any status including VALID (in the case where we want to note a * warning, for example). * See BUG 664676: GCLI conversion error messages should be localized * @param predictions If status=INCOMPLETE, there could be predictions as to * the options available to complete the input. * We generally expect there to be about 7 predictions (to match human list * comprehension ability) however it is valid to provide up to about 20, * or less. It is the job of the predictor to decide a smart cut-off. * For example if there are 4 very good matches and 4 very poor ones, * probably only the 4 very good matches should be presented. * The predictions are presented either as an array of prediction objects or as * a function which returns this array when called with no parameters. * Each prediction object has the following shape: * { * name: '...', // textual completion. i.e. what the cli uses * value: { ... }, // value behind the textual completion * incomplete: true // this completion is only partial (optional) * } * The 'incomplete' property could be used to denote a valid completion which * could have sub-values (e.g. for tree navigation). */ function Conversion(value, arg, status, message, predictions) { // The result of the conversion process. Will be null if status != VALID this.value = value; // Allow us to trace where this Conversion came from this.arg = arg; if (arg == null) { throw new Error('Missing arg'); } this._status = status || Status.VALID; this.message = message; this.predictions = predictions; } types.Conversion = Conversion; /** * Ensure that all arguments that are part of this conversion know what they * are assigned to. * @param assignment The Assignment (param/conversion link) to inform the * argument about. */ Conversion.prototype.assign = function(assignment) { this.arg.assign(assignment); }; /** * Work out if there is information provided in the contained argument. */ Conversion.prototype.isDataProvided = function() { return !this.arg.isBlank(); }; /** * 2 conversions are equal if and only if their args are equal (argEquals) and * their values are equal (valueEquals). * @param that The conversion object to compare against. */ Conversion.prototype.equals = function(that) { if (this === that) { return true; } if (that == null) { return false; } return this.valueEquals(that) && this.argEquals(that); }; /** * Check that the value in this conversion is strict equal to the value in the * provided conversion. * @param that The conversion to compare values with */ Conversion.prototype.valueEquals = function(that) { return this.value === that.value; }; /** * Check that the argument in this conversion is equal to the value in the * provided conversion as defined by the argument (i.e. arg.equals). * @param that The conversion to compare arguments with */ Conversion.prototype.argEquals = function(that) { return that == null ? false : this.arg.equals(that.arg); }; /** * Accessor for the status of this conversion */ Conversion.prototype.getStatus = function(arg) { return this._status; }; /** * Defined by the toString() value provided by the argument */ Conversion.prototype.toString = function() { return this.arg.toString(); }; /** * If status === INCOMPLETE, then we may be able to provide predictions as to * how the argument can be completed. * @return An array of items, where each item is an object with the following * properties: * - name (mandatory): Displayed to the user, and typed in. No whitespace * - description (optional): Short string for display in a tool-tip * - manual (optional): Longer description which details usage * - incomplete (optional): Indicates that the prediction if used should not * be considered necessarily sufficient, which typically will mean that the * UI should not append a space to the completion * - value (optional): If a value property is present, this will be used as the * value of the conversion, otherwise the item itself will be used. */ Conversion.prototype.getPredictions = function() { if (typeof this.predictions === 'function') { return this.predictions(); } return this.predictions || []; }; /** * Accessor for a prediction by index. * This is useful above getPredictions()[index] because it normalizes * index to be within the bounds of the predictions, which means that the UI * can maintain an index of which prediction to choose without caring how many * predictions there are. * @param index The index of the prediction to choose */ Conversion.prototype.getPredictionAt = function(index) { if (index == null) { return undefined; } var predictions = this.getPredictions(); if (predictions.length === 0) { return undefined; } index = index % predictions.length; if (index < 0) { index = predictions.length + index; } return predictions[index]; }; /** * Accessor for a prediction by index. * This is useful above getPredictions()[index] because it normalizes * index to be within the bounds of the predictions, which means that the UI * can maintain an index of which prediction to choose without caring how many * predictions there are. * @param index The index of the prediction to choose */ Conversion.prototype.constrainPredictionIndex = function(index) { if (index == null) { return undefined; } var predictions = this.getPredictions(); if (predictions.length === 0) { return undefined; } index = index % predictions.length; if (index < 0) { index = predictions.length + index; } return index; }; /** * ArrayConversion is a special Conversion, needed because arrays are converted * member by member rather then as a whole, which means we can track the * conversion if individual array elements. So an ArrayConversion acts like a * normal Conversion (which is needed as Assignment requires a Conversion) but * it can also be devolved into a set of Conversions for each array member. */ function ArrayConversion(conversions, arg) { this.arg = arg; this.conversions = conversions; this.value = conversions.map(function(conversion) { return conversion.value; }, this); this._status = Status.combine(conversions.map(function(conversion) { return conversion.getStatus(); })); // This message is just for reporting errors like "not enough values" // rather that for problems with individual values. this.message = ''; // Predictions are generally provided by individual values this.predictions = []; } ArrayConversion.prototype = Object.create(Conversion.prototype); ArrayConversion.prototype.assign = function(assignment) { this.conversions.forEach(function(conversion) { conversion.assign(assignment); }, this); this.assignment = assignment; }; ArrayConversion.prototype.getStatus = function(arg) { if (arg && arg.conversion) { return arg.conversion.getStatus(); } return this._status; }; ArrayConversion.prototype.isDataProvided = function() { return this.conversions.length > 0; }; ArrayConversion.prototype.valueEquals = function(that) { if (!(that instanceof ArrayConversion)) { throw new Error('Can\'t compare values with non ArrayConversion'); } if (this.value === that.value) { return true; } if (this.value.length !== that.value.length) { return false; } for (var i = 0; i < this.conversions.length; i++) { if (!this.conversions[i].valueEquals(that.conversions[i])) { return false; } } return true; }; ArrayConversion.prototype.toString = function() { return '[ ' + this.conversions.map(function(conversion) { return conversion.toString(); }, this).join(', ') + ' ]'; }; types.ArrayConversion = ArrayConversion; /** * Most of our types are 'static' e.g. there is only one type of 'string', * however some types like 'selection' and 'deferred' are customizable. * The basic Type type isn't useful, but does provide documentation about what * types do. */ function Type() { } /** * Convert the given value to a string representation. * Where possible, there should be round-tripping between values and their * string representations. */ Type.prototype.stringify = function(value) { throw new Error('Not implemented'); }; /** * Convert the given arg to an instance of this type. * Where possible, there should be round-tripping between values and their * string representations. * @param arg An instance of Argument to convert. * @return Conversion */ Type.prototype.parse = function(arg) { throw new Error('Not implemented'); }; /** * A convenience method for times when you don't have an argument to parse * but instead have a string. * @see #parse(arg) */ Type.prototype.parseString = function(str) { return this.parse(new Argument(str)); }, /** * The plug-in system, and other things need to know what this type is * called. The name alone is not enough to fully specify a type. Types like * 'selection' and 'deferred' need extra data, however this function returns * only the name, not the extra data. */ Type.prototype.name = undefined; /** * If there is some concept of a higher value, return it, * otherwise return undefined. */ Type.prototype.increment = function(value) { return undefined; }; /** * If there is some concept of a lower value, return it, * otherwise return undefined. */ Type.prototype.decrement = function(value) { return undefined; }; /** * The 'blank value' of most types is 'undefined', but there are exceptions; * This allows types to specify a better conversion from empty string than * 'undefined'. * 2 known examples of this are boolean -> false and array -> [] */ Type.prototype.getBlank = function() { return this.parse(new Argument()); }; /** * This is something of a hack for the benefit of DeferredType which needs to * be able to lie about it's type for fields to accept it as one of their own. * Sub-types can ignore this unless they're DeferredType. */ Type.prototype.getType = function() { return this; }; types.Type = Type; /** * Private registry of types * Invariant: types[name] = type.name */ var registeredTypes = {}; types.getTypeNames = function() { return Object.keys(registeredTypes); }; /** * Add a new type to the list available to the system. * You can pass 2 things to this function - either an instance of Type, in * which case we return this instance when #getType() is called with a 'name' * that matches type.name. * Also you can pass in a constructor (i.e. function) in which case when * #getType() is called with a 'name' that matches Type.prototype.name we will * pass the typeSpec into this constructor. */ types.registerType = function(type) { if (typeof type === 'object') { if (type instanceof Type) { if (!type.name) { throw new Error('All registered types must have a name'); } registeredTypes[type.name] = type; } else { throw new Error('Can\'t registerType using: ' + type); } } else if (typeof type === 'function') { if (!type.prototype.name) { throw new Error('All registered types must have a name'); } registeredTypes[type.prototype.name] = type; } else { throw new Error('Unknown type: ' + type); } }; types.registerTypes = function registerTypes(newTypes) { Object.keys(newTypes).forEach(function(name) { var type = newTypes[name]; type.name = name; newTypes.registerType(type); }); }; /** * Remove a type from the list available to the system */ types.deregisterType = function(type) { delete registeredTypes[type.name]; }; /** * Find a type, previously registered using #registerType() */ types.getType = function(typeSpec) { var type; if (typeof typeSpec === 'string') { type = registeredTypes[typeSpec]; if (typeof type === 'function') { type = new type({}); } return type; } if (typeof typeSpec === 'object') { if (!typeSpec.name) { throw new Error('Missing \'name\' member to typeSpec'); } type = registeredTypes[typeSpec.name]; if (typeof type === 'function') { type = new type(typeSpec); } return type; } throw new Error('Can\'t extract type from ' + typeSpec); }; }); /* * 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/argument', ['require', 'exports', 'module' ], function(require, exports, module) { var argument = exports; /** * We record where in the input string an argument comes so we can report * errors against those string positions. * @param text The string (trimmed) that contains the argument * @param prefix Knowledge of quotation marks and whitespace used prior to the * text in the input string allows us to re-generate the original input from * the arguments. * @param suffix Any quotation marks and whitespace used after the text. * Whitespace is normally placed in the prefix to the succeeding argument, but * can be used here when this is the last argument. * @constructor */ function Argument(text, prefix, suffix) { if (text === undefined) { this.text = ''; this.prefix = ''; this.suffix = ''; } else { this.text = text; this.prefix = prefix !== undefined ? prefix : ''; this.suffix = suffix !== undefined ? suffix : ''; } } /** * Return the result of merging these arguments. * case and some of the arguments are in quotation marks? */ Argument.prototype.merge = function(following) { // Is it possible that this gets called when we're merging arguments // for the single string? return new Argument( this.text + this.suffix + following.prefix + following.text, this.prefix, following.suffix); }; /** * Returns a new Argument like this one but with the text set to * replText and the end adjusted to fit. * @param replText Text to replace the old text value */ Argument.prototype.beget = function(replText, options) { var prefix = this.prefix; var suffix = this.suffix; // We need to add quotes when the replacement string has spaces or is empty var quote = (replText.indexOf(' ') >= 0 || replText.length == 0) ? '\'' : ''; if (options) { prefix = (options.prefixSpace ? ' ' : '') + quote; suffix = quote; } return new Argument(replText, prefix, suffix); }; /** * Is there any visible content to this argument? */ Argument.prototype.isBlank = function() { return this.text === '' && this.prefix.trim() === '' && this.suffix.trim() === ''; }; /** * We need to keep track of which assignment we've been assigned to */ Argument.prototype.assign = function(assignment) { this.assignment = assignment; }; /** * Sub-classes of Argument are collections of arguments, getArgs() gets access * to the members of the collection in order to do things like re-create input * command lines. For the simple Argument case it's just an array containing * only this. */ Argument.prototype.getArgs = function() { return [ this ]; }; /** * We define equals to mean all arg properties are strict equals. * Used by Conversion.argEquals and Conversion.equals and ultimately * Assignment.equals to avoid reporting a change event when a new conversion * is assigned. */ Argument.prototype.equals = function(that) { if (this === that) { return true; } if (that == null || !(that instanceof Argument)) { return false; } return this.text === that.text && this.prefix === that.prefix && this.suffix === that.suffix; }; /** * Helper when we're putting arguments back together */ Argument.prototype.toString = function() { // BUG 664207: We should re-escape escaped characters // But can we do that reliably? return this.prefix + this.text + this.suffix; }; /** * Merge an array of arguments into a single argument. * All Arguments in the array are expected to have the same emitter */ Argument.merge = function(argArray, start, end) { start = (start === undefined) ? 0 : start; end = (end === undefined) ? argArray.length : end; var joined; for (var i = start; i < end; i++) { var arg = argArray[i]; if (!joined) { joined = arg; } else { joined = joined.merge(arg); } } return joined; }; argument.Argument = Argument; /** * ScriptArgument is a marker that the argument is designed to be Javascript. * It also implements the special rules that spaces after the { or before the * } are part of the pre/suffix rather than the content, and that they are * never 'blank' so they can be used by Requisition._split() and not raise an * ERROR status due to being blank. */ function ScriptArgument(text, prefix, suffix) { this.text = text !== undefined ? text : ''; this.prefix = prefix !== undefined ? prefix : ''; this.suffix = suffix !== undefined ? suffix : ''; while (this.text.charAt(0) === ' ') { this.prefix = this.prefix + ' '; this.text = this.text.substring(1); } while (this.text.charAt(this.text.length - 1) === ' ') { this.suffix = ' ' + this.suffix; this.text = this.text.slice(0, -1); } } ScriptArgument.prototype = Object.create(Argument.prototype); /** * Returns a new Argument like this one but with the text set to * replText and the end adjusted to fit. * @param replText Text to replace the old text value */ ScriptArgument.prototype.beget = function(replText, options) { var prefix = this.prefix; var suffix = this.suffix; if (options && options.normalize) { prefix = '{ '; suffix = ' }'; } return new ScriptArgument(replText, prefix, suffix); }; /** * ScriptArguments are never blank due to the '{' and '}' and their special use * for the command argument requires them not to be blank even when there is * no text. */ ScriptArgument.prototype.isBlank = function() { return false; }; argument.ScriptArgument = ScriptArgument; /** * Commands like 'echo' with a single string argument, and used with the * special format like: 'echo a b c' effectively have a number of arguments * merged together. */ function MergedArgument(args, start, end) { if (!Array.isArray(args)) { throw new Error('args is not an array of Arguments'); } if (start === undefined) { this.args = args; } else { this.args = args.slice(start, end); } var arg = Argument.merge(this.args); this.text = arg.text; this.prefix = arg.prefix; this.suffix = arg.suffix; } MergedArgument.prototype = Object.create(Argument.prototype); /** * Keep track of which assignment we've been assigned to, and allow the * original args to do the same. */ MergedArgument.prototype.assign = function(assignment) { this.args.forEach(function(arg) { arg.assign(assignment); }, this); this.assignment = assignment; }; MergedArgument.prototype.getArgs = function() { return this.args; }; MergedArgument.prototype.equals = function(that) { if (this === that) { return true; } if (that == null || !(that instanceof MergedArgument)) { return false; } // We might need to add a check that args is the same here return this.text === that.text && this.prefix === that.prefix && this.suffix === that.suffix; }; argument.MergedArgument = MergedArgument; /** * TrueNamedArguments are for when we have an argument like --verbose which * has a boolean value, and thus the opposite of '--verbose' is ''. */ function TrueNamedArgument(name, arg) { this.arg = arg; this.text = arg ? arg.text : '--' + name; this.prefix = arg ? arg.prefix : ' '; this.suffix = arg ? arg.suffix : ''; } TrueNamedArgument.prototype = Object.create(Argument.prototype); TrueNamedArgument.prototype.assign = function(assignment) { if (this.arg) { this.arg.assign(assignment); } this.assignment = assignment; }; TrueNamedArgument.prototype.getArgs = function() { // NASTY! getArgs has a fairly specific use: in removing used arguments // from a command line. Unlike other arguments which are EITHER used // in assignments directly OR grouped in things like MergedArguments, // TrueNamedArgument is used raw from the UI, or composed of another arg // from the CLI, so we return both here so they can both be removed. return this.arg ? [ this, this.arg ] : [ this ]; }; TrueNamedArgument.prototype.equals = function(that) { if (this === that) { return true; } if (that == null || !(that instanceof TrueNamedArgument)) { return false; } return this.text === that.text && this.prefix === that.prefix && this.suffix === that.suffix; }; argument.TrueNamedArgument = TrueNamedArgument; /** * FalseNamedArguments are for when we don't have an argument like --verbose * which has a boolean value, and thus the opposite of '' is '--verbose'. */ function FalseNamedArgument() { this.text = ''; this.prefix = ''; this.suffix = ''; } FalseNamedArgument.prototype = Object.create(Argument.prototype); FalseNamedArgument.prototype.getArgs = function() { return [ ]; }; FalseNamedArgument.prototype.equals = function(that) { if (this === that) { return true; } if (that == null || !(that instanceof FalseNamedArgument)) { return false; } return this.text === that.text && this.prefix === that.prefix && this.suffix === that.suffix; }; argument.FalseNamedArgument = FalseNamedArgument; /** * A named argument is for cases where we have input in one of the following * formats: * * The general format is: * /--?{unique-param-name-prefix}[ :=]{value}/ * We model this as a normal argument but with a long prefix. */ function NamedArgument(nameArg, valueArg) { this.nameArg = nameArg; this.valueArg = valueArg; this.text = valueArg.text; this.prefix = nameArg.toString() + valueArg.prefix; this.suffix = valueArg.suffix; } NamedArgument.prototype = Object.create(Argument.prototype); NamedArgument.prototype.assign = function(assignment) { this.nameArg.assign(assignment); this.valueArg.assign(assignment); this.assignment = assignment; }; NamedArgument.prototype.getArgs = function() { return [ this.nameArg, this.valueArg ]; }; NamedArgument.prototype.equals = function(that) { if (this === that) { return true; } if (that == null) { return false; } if (!(that instanceof NamedArgument)) { return false; } // We might need to add a check that nameArg and valueArg are the same return this.text === that.text && this.prefix === that.prefix && this.suffix === that.suffix; }; argument.NamedArgument = NamedArgument; /** * An argument the groups together a number of plain arguments together so they * can be jointly assigned to a single array parameter */ function ArrayArgument() { this.args = []; } ArrayArgument.prototype = Object.create(Argument.prototype); ArrayArgument.prototype.addArgument = function(arg) { this.args.push(arg); }; ArrayArgument.prototype.addArguments = function(args) { Array.prototype.push.apply(this.args, args); }; ArrayArgument.prototype.getArguments = function() { return this.args; }; ArrayArgument.prototype.assign = function(assignment) { this.args.forEach(function(arg) { arg.assign(assignment); }, this); this.assignment = assignment; }; ArrayArgument.prototype.getArgs = function() { return this.args; }; ArrayArgument.prototype.equals = function(that) { if (this === that) { return true; } if (that == null) { return false; } if (!(that instanceof ArrayArgument)) { return false; } if (this.args.length !== that.args.length) { return false; } for (var i = 0; i < this.args.length; i++) { if (!this.args[i].equals(that.args[i])) { return false; } } return true; }; /** * Helper when we're putting arguments back together */ ArrayArgument.prototype.toString = function() { return '{' + this.args.map(function(arg) { return arg.toString(); }, this).join(',') + '}'; }; argument.ArrayArgument = ArrayArgument; }); /* * 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/types/basic', ['require', 'exports', 'module' , 'gcli/l10n', 'gcli/types', 'gcli/types/spell', 'gcli/types/selection', 'gcli/argument'], function(require, exports, module) { var l10n = require('gcli/l10n'); var types = require('gcli/types'); var Type = require('gcli/types').Type; var Status = require('gcli/types').Status; var Conversion = require('gcli/types').Conversion; var ArrayConversion = require('gcli/types').ArrayConversion; var Speller = require('gcli/types/spell').Speller; var SelectionType = require('gcli/types/selection').SelectionType; var Argument = require('gcli/argument').Argument; var TrueNamedArgument = require('gcli/argument').TrueNamedArgument; var FalseNamedArgument = require('gcli/argument').FalseNamedArgument; var ArrayArgument = require('gcli/argument').ArrayArgument; /** * Registration and de-registration. */ exports.startup = function() { types.registerType(StringType); types.registerType(NumberType); types.registerType(BooleanType); types.registerType(BlankType); types.registerType(DeferredType); types.registerType(ArrayType); }; exports.shutdown = function() { types.unregisterType(StringType); types.unregisterType(NumberType); types.unregisterType(BooleanType); types.unregisterType(BlankType); types.unregisterType(DeferredType); types.unregisterType(ArrayType); }; /** * 'string' the most basic string type that doesn't need to convert */ function StringType(typeSpec) { if (Object.keys(typeSpec).length > 0) { throw new Error('StringType can not be customized'); } } StringType.prototype = Object.create(Type.prototype); StringType.prototype.stringify = function(value) { if (value == null) { return ''; } return value.toString(); }; StringType.prototype.parse = function(arg) { if (arg.text == null || arg.text === '') { return new Conversion(undefined, arg, Status.INCOMPLETE, ''); } return new Conversion(arg.text, arg); }; StringType.prototype.name = 'string'; exports.StringType = StringType; /** * We don't currently plan to distinguish between integers and floats */ function NumberType(typeSpec) { if (typeSpec) { this._min = typeSpec.min; this._max = typeSpec.max; this._step = typeSpec.step || 1; } else { this._step = 1; } } NumberType.prototype = Object.create(Type.prototype); NumberType.prototype.stringify = function(value) { if (value == null) { return ''; } return '' + value; }; NumberType.prototype.getMin = function() { if (this._min) { if (typeof this._min === 'function') { return this._min(); } if (typeof this._min === 'number') { return this._min; } } return undefined; }; NumberType.prototype.getMax = function() { if (this._max) { if (typeof this._max === 'function') { return this._max(); } if (typeof this._max === 'number') { return this._max; } } return undefined; }; NumberType.prototype.parse = function(arg) { if (arg.text.replace(/\s/g, '').length === 0) { return new Conversion(undefined, arg, Status.INCOMPLETE, ''); } var value = parseInt(arg.text, 10); if (isNaN(value)) { return new Conversion(undefined, arg, Status.ERROR, l10n.lookupFormat('typesNumberNan', [ arg.text ])); } var max = this.getMax(); if (max != null && value > max) { return new Conversion(undefined, arg, Status.ERROR, l10n.lookupFormat('typesNumberMax', [ value, max ])); } var min = this.getMin(); if (min != null && value < min) { return new Conversion(undefined, arg, Status.ERROR, l10n.lookupFormat('typesNumberMin', [ value, min ])); } return new Conversion(value, arg); }; NumberType.prototype.decrement = function(value) { if (typeof value !== 'number' || isNaN(value)) { return this.getMax() || 1; } var newValue = value - this._step; // Snap to the nearest incremental of the step newValue = Math.ceil(newValue / this._step) * this._step; return this._boundsCheck(newValue); }; NumberType.prototype.increment = function(value) { if (typeof value !== 'number' || isNaN(value)) { var min = this.getMin(); return min != null ? min : 0; } var newValue = value + this._step; // Snap to the nearest incremental of the step newValue = Math.floor(newValue / this._step) * this._step; if (this.getMax() == null) { return newValue; } return this._boundsCheck(newValue); }; /** * Return the input value so long as it is within the max/min bounds. If it is * lower than the minimum, return the minimum. If it is bigger than the maximum * then return the maximum. */ NumberType.prototype._boundsCheck = function(value) { var min = this.getMin(); if (min != null && value < min) { return min; } var max = this.getMax(); if (max != null && value > max) { return max; } return value; }; NumberType.prototype.name = 'number'; exports.NumberType = NumberType; /** * true/false values */ function BooleanType(typeSpec) { if (Object.keys(typeSpec).length > 0) { throw new Error('BooleanType can not be customized'); } } BooleanType.prototype = Object.create(SelectionType.prototype); BooleanType.prototype.lookup = [ { name: 'false', value: false }, { name: 'true', value: true } ]; BooleanType.prototype.parse = function(arg) { if (arg instanceof TrueNamedArgument) { return new Conversion(true, arg); } if (arg instanceof FalseNamedArgument) { return new Conversion(false, arg); } return SelectionType.prototype.parse.call(this, arg); }; BooleanType.prototype.stringify = function(value) { return '' + value; }; BooleanType.prototype.getBlank = function() { return new Conversion(false, new Argument(), Status.VALID, '', this.lookup); }; BooleanType.prototype.name = 'boolean'; exports.BooleanType = BooleanType; /** * A type for "we don't know right now, but hope to soon". */ function DeferredType(typeSpec) { if (typeof typeSpec.defer !== 'function') { throw new Error('Instances of DeferredType need typeSpec.defer to be a function that returns a type'); } Object.keys(typeSpec).forEach(function(key) { this[key] = typeSpec[key]; }, this); } DeferredType.prototype = Object.create(Type.prototype); DeferredType.prototype.stringify = function(value) { return this.defer().stringify(value); }; DeferredType.prototype.parse = function(arg) { return this.defer().parse(arg); }; DeferredType.prototype.decrement = function(value) { var deferred = this.defer(); return (deferred.decrement ? deferred.decrement(value) : undefined); }; DeferredType.prototype.increment = function(value) { var deferred = this.defer(); return (deferred.increment ? deferred.increment(value) : undefined); }; DeferredType.prototype.increment = function(value) { var deferred = this.defer(); return (deferred.increment ? deferred.increment(value) : undefined); }; DeferredType.prototype.getType = function() { return this.defer(); }; Object.defineProperty(DeferredType.prototype, 'isImportant', { get: function() { return this.defer().isImportant; }, enumerable: true }); DeferredType.prototype.name = 'deferred'; exports.DeferredType = DeferredType; /** * 'blank' is a type for use with DeferredType when we don't know yet. * It should not be used anywhere else. */ function BlankType(typeSpec) { if (Object.keys(typeSpec).length > 0) { throw new Error('BlankType can not be customized'); } } BlankType.prototype = Object.create(Type.prototype); BlankType.prototype.stringify = function(value) { return ''; }; BlankType.prototype.parse = function(arg) { return new Conversion(undefined, arg); }; BlankType.prototype.name = 'blank'; exports.BlankType = BlankType; /** * A set of objects of the same type */ function ArrayType(typeSpec) { if (!typeSpec.subtype) { console.error('Array.typeSpec is missing subtype. Assuming string.' + JSON.stringify(typeSpec)); typeSpec.subtype = 'string'; } Object.keys(typeSpec).forEach(function(key) { this[key] = typeSpec[key]; }, this); this.subtype = types.getType(this.subtype); } ArrayType.prototype = Object.create(Type.prototype); ArrayType.prototype.stringify = function(values) { // BUG 664204: Check for strings with spaces and add quotes return values.join(' '); }; ArrayType.prototype.parse = function(arg) { if (arg instanceof ArrayArgument) { var conversions = arg.getArguments().map(function(subArg) { var conversion = this.subtype.parse(subArg); // Hack alert. ArrayConversion needs to be able to answer questions // about the status of individual conversions in addition to the // overall state. This allows us to do that easily. subArg.conversion = conversion; return conversion; }, this); return new ArrayConversion(conversions, arg); } else { console.error('non ArrayArgument to ArrayType.parse', arg); throw new Error('non ArrayArgument to ArrayType.parse'); } }; ArrayType.prototype.getBlank = function(values) { return new ArrayConversion([], new ArrayArgument()); }; ArrayType.prototype.name = 'array'; exports.ArrayType = ArrayType; }); /* * Copyright (c) 2009 Panagiotis Astithas * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ define('gcli/types/spell', ['require', 'exports', 'module' ], function(require, exports, module) { /** * A spell-checker based on the statistical algorithm described by Peter Norvig * in http://norvig.com/spell-correct.html, and converted to JavaScript by Past * http://past.github.com/speller/ * * Usage requires a two-step process: * 1) call speller.train() one or more times with a large text to train the * language model * 2) call speller.correct(word) to retrieve the correction for the specified * word */ function Speller() { // A map of words to the count of times they were encountered during training. this._nWords = {}; } Speller.letters = "abcdefghijklmnopqrstuvwxyz".split(""); /** * A function that trains the language model with the words in the supplied * text. Multiple invocation of this function can extend the training of the * model. */ Speller.prototype.train = function(words) { words.forEach(function(word) { word = word.toLowerCase(); this._nWords[word] = this._nWords.hasOwnProperty(word) ? this._nWords[word] + 1 : 1; }, this); }; /** * A function that returns the correction for the specified word. */ Speller.prototype.correct = function(word) { if (this._nWords.hasOwnProperty(word)) { return word; } var candidates = {}; var list = this._edits(word); list.forEach(function(edit) { if (this._nWords.hasOwnProperty(edit)) { candidates[this._nWords[edit]] = edit; } }, this); if (this._countKeys(candidates) > 0) { return candidates[this._max(candidates)]; } list.forEach(function(edit) { this._edits(edit).forEach(function(w) { if (this._nWords.hasOwnProperty(w)) { candidates[this._nWords[w]] = w; } }, this); }, this); return this._countKeys(candidates) > 0 ? candidates[this._max(candidates)] : null; }; /** * A helper function that counts the keys in the supplied object. */ Speller.prototype._countKeys = function(object) { // return Object.keys(object).length; ? var count = 0; for (var attr in object) { if (object.hasOwnProperty(attr)) { count++; } } return count; }; /** * A helper function that returns the word with the most occurrences in the * language model, among the supplied candidates. * @param candidates */ Speller.prototype._max = function(candidates) { var arr = []; for (var candidate in candidates) { if (candidates.hasOwnProperty(candidate)) { arr.push(candidate); } } return Math.max.apply(null, arr); }; /** * A function that returns the set of possible corrections of the specified * word. The edits can be deletions, insertions, alterations or transpositions. */ Speller.prototype._edits = function(word) { var results = []; // Deletion for (var i = 0; i < word.length; i++) { results.push(word.slice(0, i) + word.slice(i + 1)); } // Transposition for (i = 0; i < word.length - 1; i++) { results.push(word.slice(0, i) + word.slice(i + 1, i + 2) + word.slice(i, i + 1) + word.slice(i + 2)); } // Alteration for (i = 0; i < word.length; i++) { Speller.letters.forEach(function(l) { results.push(word.slice(0, i) + l + word.slice(i + 1)); }, this); } // Insertion for (i = 0; i <= word.length; i++) { Speller.letters.forEach(function(l) { results.push(word.slice(0, i) + l + word.slice(i)); }, this); } return results; }; exports.Speller = Speller; }); /* * 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/types/selection', ['require', 'exports', 'module' , 'gcli/l10n', 'gcli/types', 'gcli/types/spell', 'gcli/argument'], function(require, exports, module) { var l10n = require('gcli/l10n'); var types = require('gcli/types'); var Type = require('gcli/types').Type; var Status = require('gcli/types').Status; var Conversion = require('gcli/types').Conversion; var Speller = require('gcli/types/spell').Speller; var Argument = require('gcli/argument').Argument; /** * Registration and de-registration. */ exports.startup = function() { types.registerType(SelectionType); }; exports.shutdown = function() { types.unregisterType(SelectionType); }; /** * A selection allows the user to pick a value from known set of options. * An option is made up of a name (which is what the user types) and a value * (which is passed to exec) * @param typeSpec Object containing properties that describe how this * selection functions. Properties include: * - lookup: An array of objects, one for each option, which contain name and * value properties. lookup can be a function which returns this array * - data: An array of strings - alternative to 'lookup' where the valid values * are strings. i.e. there is no mapping between what is typed and the value * that is used by the program * - stringifyProperty: Conversion from value to string is generally a process * of looking through all the valid options for a matching value, and using * the associated name. However the name maybe available directly from the * value using a property lookup. Setting 'stringifyProperty' allows * SelectionType to take this shortcut. */ function SelectionType(typeSpec) { if (typeSpec) { Object.keys(typeSpec).forEach(function(key) { this[key] = typeSpec[key]; }, this); } } SelectionType.prototype = Object.create(Type.prototype); SelectionType.prototype.maxPredictions = 10; SelectionType.prototype.stringify = function(value) { if (this.stringifyProperty != null) { return value[this.stringifyProperty]; } var name = null; var lookup = this.getLookup(); lookup.some(function(item) { if (item.value === value) { name = item.name; return true; } return false; }, this); return name; }; /** * There are several ways to get selection data. This unifies them into one * single function. * @return An array of objects with name and value properties. */ SelectionType.prototype.getLookup = function() { if (this.lookup) { if (typeof this.lookup === 'function') { return this.lookup(); } return this.lookup; } if (Array.isArray(this.data)) { this.lookup = this._dataToLookup(this.data); return this.lookup; } if (typeof(this.data) === 'function') { return this._dataToLookup(this.data()); } throw new Error('SelectionType has no data'); }; /** * Selection can be provided with either a lookup object (in the 'lookup' * property) or an array of strings (in the 'data' property). Internally we * always use lookup, so we need a way to convert a 'data' array to a lookup. */ SelectionType.prototype._dataToLookup = function(data) { return data.map(function(option) { return { name: option, value: option }; }, this); }; /** * Return a list of possible completions for the given arg. * @param arg The initial input to match * @return A trimmed array of string:value pairs */ SelectionType.prototype._findPredictions = function(arg) { var predictions = []; var lookup = this.getLookup(); var i, option; // If the arg has a suffix then we're kind of 'done'. Only an exact match // will do. if (arg.suffix.length > 0) { for (i = 0; i < lookup.length && predictions.length < this.maxPredictions; i++) { option = lookup[i]; if (option.name === arg.text) { this._addToPredictions(predictions, option, arg); } } return predictions; } // Start with prefix matching for (i = 0; i < lookup.length && predictions.length < this.maxPredictions; i++) { option = lookup[i]; if (option.name.indexOf(arg.text) === 0) { this._addToPredictions(predictions, option, arg); } } // Try infix matching if we get less half max matched if (predictions.length < (this.maxPredictions / 2)) { for (i = 0; i < lookup.length && predictions.length < this.maxPredictions; i++) { option = lookup[i]; if (option.name.indexOf(arg.text) !== -1) { if (predictions.indexOf(option) === -1) { this._addToPredictions(predictions, option, arg); } } } } // Try fuzzy matching if we don't get a prefix match if (false && predictions.length === 0) { var speller = new Speller(); var names = lookup.map(function(opt) { return opt.name; }); speller.train(names); var corrected = speller.correct(arg.text); if (corrected) { lookup.forEach(function(opt) { if (opt.name === corrected) { predictions.push(opt); } }, this); } } return predictions; }; /** * Add an option to our list of predicted options. * We abstract out this portion of _findPredictions() because CommandType needs * to make an extra check before actually adding which SelectionType does not * need to make. */ SelectionType.prototype._addToPredictions = function(predictions, option, arg) { predictions.push(option); }; SelectionType.prototype.parse = function(arg) { var predictions = this._findPredictions(arg); if (predictions.length === 0) { var msg = l10n.lookupFormat('typesSelectionNomatch', [ arg.text ]); return new Conversion(undefined, arg, Status.ERROR, msg, predictions); } // This is something of a hack it basically allows us to tell the // setting type to forget its last setting hack. if (this.noMatch) { this.noMatch(); } var value = predictions[0].value; if (predictions[0].name === arg.text) { return new Conversion(value, arg, Status.VALID, '', predictions); } return new Conversion(undefined, arg, Status.INCOMPLETE, '', predictions); }; /** * For selections, up is down and black is white. It's like this, given a list * [ a, b, c, d ], it's natural to think that it starts at the top and that * going up the list, moves towards 'a'. However 'a' has the lowest index, so * for SelectionType, up is down and down is up. * Sorry. */ SelectionType.prototype.decrement = function(value) { var lookup = this.getLookup(); var index = this._findValue(lookup, value); if (index === -1) { index = 0; } index++; if (index >= lookup.length) { index = 0; } return lookup[index].value; }; /** * See note on SelectionType.decrement() */ SelectionType.prototype.increment = function(value) { var lookup = this.getLookup(); var index = this._findValue(lookup, value); if (index === -1) { // For an increment operation when there is nothing to start from, we // want to start from the top, i.e. index 0, so the value before we // 'increment' (see note above) must be 1. index = 1; } index--; if (index < 0) { index = lookup.length - 1; } return lookup[index].value; }; /** * Walk through an array of { name:.., value:... } objects looking for a * matching value (using strict equality), returning the matched index (or -1 * if not found). * @param lookup Array of objects with name/value properties to search through * @param value The value to search for * @return The index at which the match was found, or -1 if no match was found */ SelectionType.prototype._findValue = function(lookup, value) { var index = -1; for (var i = 0; i < lookup.length; i++) { var pair = lookup[i]; if (pair.value === value) { index = i; break; } } return index; }; SelectionType.prototype.name = 'selection'; exports.SelectionType = SelectionType; }); /* * 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/types/command', ['require', 'exports', 'module' , 'gcli/canon', 'gcli/l10n', 'gcli/types', 'gcli/types/selection'], function(require, exports, module) { var canon = require('gcli/canon'); var l10n = require('gcli/l10n'); var types = require('gcli/types'); var SelectionType = require('gcli/types/selection').SelectionType; var Status = require('gcli/types').Status; var Conversion = require('gcli/types').Conversion; /** * Registration and de-registration. */ exports.startup = function() { types.registerType(CommandType); }; exports.shutdown = function() { types.unregisterType(CommandType); }; /** * Select from the available commands. * This is very similar to a SelectionType, however the level of hackery in * SelectionType to make it handle Commands correctly was to high, so we * simplified. * If you are making changes to this code, you should check there too. */ function CommandType() { this.stringifyProperty = 'name'; } CommandType.prototype = Object.create(SelectionType.prototype); CommandType.prototype.name = 'command'; CommandType.prototype.lookup = function() { var commands = canon.getCommands(); commands.sort(function(c1, c2) { return c1.name.localeCompare(c2.name); }); return commands.map(function(command) { return { name: command.name, value: command }; }, this); }; /** * Add an option to our list of predicted options */ CommandType.prototype._addToPredictions = function(predictions, option, arg) { if (option.value.hidden) { return; } // The command type needs to exclude sub-commands when the CLI // is blank, but include them when we're filtering. This hack // excludes matches when the filter text is '' and when the // name includes a space. if (arg.text.length !== 0 || option.name.indexOf(' ') === -1) { predictions.push(option); } }; CommandType.prototype.parse = function(arg) { // Especially at startup, predictions live over the time that things change // so we provide a completion function rather than completion values var predictFunc = function() { return this._findPredictions(arg); }.bind(this); var predictions = this._findPredictions(arg); if (predictions.length === 0) { var msg = l10n.lookupFormat('typesSelectionNomatch', [ arg.text ]); return new Conversion(undefined, arg, Status.ERROR, msg, predictFunc); } var command = predictions[0].value; if (predictions.length === 1) { // Is it an exact match of an executable command, // or just the only possibility? if (command.name === arg.text && typeof command.exec === 'function') { return new Conversion(command, arg, Status.VALID, ''); } return new Conversion(undefined, arg, Status.INCOMPLETE, '', predictFunc); } // It's valid if the text matches, even if there are several options if (predictions[0].name === arg.text) { return new Conversion(command, arg, Status.VALID, '', predictFunc); } return new Conversion(undefined, arg, Status.INCOMPLETE, '', predictFunc); }; }); /* * 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/types/javascript', ['require', 'exports', 'module' , 'gcli/l10n', 'gcli/types'], function(require, exports, module) { var l10n = require('gcli/l10n'); var types = require('gcli/types'); var Conversion = types.Conversion; var Type = types.Type; var Status = types.Status; /** * Registration and de-registration. */ exports.startup = function() { types.registerType(JavascriptType); }; exports.shutdown = function() { types.unregisterType(JavascriptType); }; /** * The object against which we complete, which is usually 'window' if it exists * but could be something else in non-web-content environments. */ var globalObject; if (typeof window !== 'undefined') { globalObject = window; } /** * Setter for the object against which JavaScript completions happen */ exports.setGlobalObject = function(obj) { globalObject = obj; }; /** * Getter for the object against which JavaScript completions happen, for use * in testing */ exports.getGlobalObject = function() { return globalObject; }; /** * Remove registration of object against which JavaScript completions happen */ exports.unsetGlobalObject = function() { globalObject = undefined; }; /** * 'javascript' handles scripted input */ function JavascriptType(typeSpec) { if (Object.keys(typeSpec).length > 0) { throw new Error('JavascriptType can not be customized'); } } JavascriptType.prototype = Object.create(Type.prototype); JavascriptType.prototype.stringify = function(value) { if (value == null) { return ''; } return value; }; /** * When sorting out completions, there is no point in displaying millions of * matches - this the number of matches that we aim for */ JavascriptType.MAX_COMPLETION_MATCHES = 10; JavascriptType.prototype.parse = function(arg) { var typed = arg.text; var scope = globalObject; // No input is undefined if (typed === '') { return new Conversion(undefined, arg, Status.INCOMPLETE); } // Just accept numbers if (!isNaN(parseFloat(typed)) && isFinite(typed)) { return new Conversion(typed, arg); } // Just accept constants like true/false/null/etc if (typed.trim().match(/(null|undefined|NaN|Infinity|true|false)/)) { return new Conversion(typed, arg); } // Analyze the input text and find the beginning of the last part that // should be completed. var beginning = this._findCompletionBeginning(typed); // There was an error analyzing the string. if (beginning.err) { return new Conversion(typed, arg, Status.ERROR, beginning.err); } // If the current state is ParseState.COMPLEX, then we can't do completion. // so bail out now if (beginning.state === ParseState.COMPLEX) { return new Conversion(typed, arg); } // If the current state is not ParseState.NORMAL, then we are inside of a // string which means that no completion is possible. if (beginning.state !== ParseState.NORMAL) { return new Conversion(typed, arg, Status.INCOMPLETE, ''); } var completionPart = typed.substring(beginning.startPos); var properties = completionPart.split('.'); var matchProp; var prop; if (properties.length > 1) { matchProp = properties.pop().trimLeft(); for (var i = 0; i < properties.length; i++) { prop = properties[i].trim(); // We can't complete on null.foo, so bail out if (scope == null) { return new Conversion(typed, arg, Status.ERROR, l10n.lookup('jstypeParseScope')); } if (prop === '') { return new Conversion(typed, arg, Status.INCOMPLETE, ''); } // Check if prop is a getter function on 'scope'. Functions can change // other stuff so we can't execute them to get the next object. Stop here. if (this._isSafeProperty(scope, prop)) { return new Conversion(typed, arg); } try { scope = scope[prop]; } catch (ex) { // It would be nice to be able to report this error in some way but // as it can happen just when someone types '{sessionStorage.', it // almost doesn't really count as an error, so we ignore it return new Conversion(typed, arg, Status.VALID, ''); } } } else { matchProp = properties[0].trimLeft(); } // If the reason we just stopped adjusting the scope was a non-simple string, // then we're not sure if the input is valid or invalid, so accept it if (prop && !prop.match(/^[0-9A-Za-z]*$/)) { return new Conversion(typed, arg); } // However if the prop was a simple string, it is an error if (scope == null) { return new Conversion(typed, arg, Status.ERROR, l10n.lookupFormat('jstypeParseMissing', [ prop ])); } // If the thing we're looking for isn't a simple string, then we're not going // to find it, but we're not sure if it's valid or invalid, so accept it if (!matchProp.match(/^[0-9A-Za-z]*$/)) { return new Conversion(typed, arg); } // Skip Iterators and Generators. if (this._isIteratorOrGenerator(scope)) { return null; } var matchLen = matchProp.length; var prefix = matchLen === 0 ? typed : typed.slice(0, -matchLen); var status = Status.INCOMPLETE; var message = ''; // We really want an array of matches (for sorting) but it's easier to // detect existing members if we're using a map initially var matches = {}; // We only display a maximum of MAX_COMPLETION_MATCHES, so there is no point // in digging up the prototype chain for matches that we're never going to // use. Initially look for matches directly on the object itself and then // look up the chain to find more var distUpPrototypeChain = 0; var root = scope; try { while (root != null && Object.keys(matches).length < JavascriptType.MAX_COMPLETION_MATCHES) { Object.keys(root).forEach(function(property) { // Only add matching properties. Also, as we're walking up the // prototype chain, properties on 'higher' prototypes don't override // similarly named properties lower down if (property.indexOf(matchProp) === 0 && !(property in matches)) { matches[property] = { prop: property, distUpPrototypeChain: distUpPrototypeChain }; } }); distUpPrototypeChain++; root = Object.getPrototypeOf(root); } } catch (ex) { return new Conversion(typed, arg, Status.INCOMPLETE, ''); } // Convert to an array for sorting, and while we're at it, note if we got // an exact match so we know that this input is valid matches = Object.keys(matches).map(function(property) { if (property === matchProp) { status = Status.VALID; } return matches[property]; }); // The sort keys are: // - Being on the object itself, not in the prototype chain // - The lack of existence of a vendor prefix // - The name matches.sort(function(m1, m2) { if (m1.distUpPrototypeChain !== m2.distUpPrototypeChain) { return m1.distUpPrototypeChain - m2.distUpPrototypeChain; } // Push all vendor prefixes to the bottom of the list return isVendorPrefixed(m1.prop) ? (isVendorPrefixed(m2.prop) ? m1.prop.localeCompare(m2.prop) : 1) : (isVendorPrefixed(m2.prop) ? -1 : m1.prop.localeCompare(m2.prop)); }); // Trim to size. There is a bug for doing a better job of finding matches // (bug 682694), but in the mean time there is a performance problem // associated with creating a large number of DOM nodes that few people will // ever read, so trim ... if (matches.length > JavascriptType.MAX_COMPLETION_MATCHES) { matches = matches.slice(0, JavascriptType.MAX_COMPLETION_MATCHES - 1); } // Decorate the matches with: // - a description // - a value (for the menu) and, // - an incomplete flag which reports if we should assume that the user isn't // going to carry on the JS expression with this input so far var predictions = matches.map(function(match) { var description; var incomplete = true; if (this._isSafeProperty(scope, match.prop)) { description = '(property getter)'; } else { try { var value = scope[match.prop]; if (typeof value === 'function') { description = '(function)'; } else if (typeof value === 'boolean' || typeof value === 'number') { description = '= ' + value; incomplete = false; } else if (typeof value === 'string') { if (value.length > 40) { value = value.substring(0, 37) + '…'; } description = '= \'' + value + '\''; incomplete = false; } else { description = '(' + typeof value + ')'; } } catch (ex) { description = '(' + l10n.lookup('jstypeParseError') + ')'; } } return { name: prefix + match.prop, value: { name: prefix + match.prop, description: description }, description: description, incomplete: incomplete }; }, this); if (predictions.length === 0) { status = Status.ERROR; message = l10n.lookupFormat('jstypeParseMissing', [ matchProp ]); } // If the match is the only one possible, and its VALID, predict nothing if (predictions.length === 1 && status === Status.VALID) { predictions = undefined; } return new Conversion(typed, arg, status, message, predictions); }; /** * Does the given property have a prefix that indicates that it is vendor * specific? */ function isVendorPrefixed(name) { return name.indexOf('moz') === 0 || name.indexOf('webkit') === 0 || name.indexOf('ms') === 0; } /** * Constants used in return value of _findCompletionBeginning() */ var ParseState = { /** * We have simple input like window.foo, without any punctuation that makes * completion prediction be confusing or wrong */ NORMAL: 0, /** * The cursor is in some Javascript that makes completion hard to predict, * like console.log( */ COMPLEX: 1, /** * The cursor is inside single quotes (') */ QUOTE: 2, /** * The cursor is inside single quotes (") */ DQUOTE: 3 }; var OPEN_BODY = '{[('.split(''); var CLOSE_BODY = '}])'.split(''); var OPEN_CLOSE_BODY = { '{': '}', '[': ']', '(': ')' }; /** * How we distinguish between simple and complex JS input. We attempt * completion against simple JS. */ var simpleChars = /[a-zA-Z0-9.]/; /** * Analyzes a given string to find the last statement that is interesting for * later completion. * @param text A string to analyze * @return If there was an error in the string detected, then a object like * { err: 'ErrorMesssage' } * is returned, otherwise a object like * { * state: ParseState.NORMAL|ParseState.QUOTE|ParseState.DQUOTE, * startPos: index of where the last statement begins * } */ JavascriptType.prototype._findCompletionBeginning = function(text) { var bodyStack = []; var state = ParseState.NORMAL; var start = 0; var c; var complex = false; for (var i = 0; i < text.length; i++) { c = text[i]; if (!simpleChars.test(c)) { complex = true; } switch (state) { // Normal JS state. case ParseState.NORMAL: if (c === '"') { state = ParseState.DQUOTE; } else if (c === '\'') { state = ParseState.QUOTE; } else if (c === ';') { start = i + 1; } else if (c === ' ') { start = i + 1; } else if (OPEN_BODY.indexOf(c) != -1) { bodyStack.push({ token: c, start: start }); start = i + 1; } else if (CLOSE_BODY.indexOf(c) != -1) { var last = bodyStack.pop(); if (!last || OPEN_CLOSE_BODY[last.token] != c) { return { err: l10n.lookup('jstypeBeginSyntax') }; } if (c === '}') { start = i + 1; } else { start = last.start; } } break; // Double quote state > " < case ParseState.DQUOTE: if (c === '\\') { i ++; } else if (c === '\n') { return { err: l10n.lookup('jstypeBeginUnterm') }; } else if (c === '"') { state = ParseState.NORMAL; } break; // Single quote state > ' < case ParseState.QUOTE: if (c === '\\') { i ++; } else if (c === '\n') { return { err: l10n.lookup('jstypeBeginUnterm') }; } else if (c === '\'') { state = ParseState.NORMAL; } break; } } if (state === ParseState.NORMAL && complex) { state = ParseState.COMPLEX; } return { state: state, startPos: start }; }; /** * Return true if the passed object is either an iterator or a generator, and * false otherwise * @param obj The object to check */ JavascriptType.prototype._isIteratorOrGenerator = function(obj) { if (obj === null) { return false; } if (typeof aObject === 'object') { if (typeof obj.__iterator__ === 'function' || obj.constructor && obj.constructor.name === 'Iterator') { return true; } try { var str = obj.toString(); if (typeof obj.next === 'function' && str.indexOf('[object Generator') === 0) { return true; } } catch (ex) { // window.history.next throws in the typeof check above. return false; } } return false; }; /** * Would calling 'scope[prop]' cause the invocation of a non-native (i.e. user * defined) function property? * Since calling functions can have side effects, it's only safe to do that if * explicitly requested, rather than because we're trying things out for the * purposes of completion. */ JavascriptType.prototype._isSafeProperty = function(scope, prop) { if (typeof scope !== 'object') { return false; } // Walk up the prototype chain of 'scope' looking for a property descriptor // for 'prop' var propDesc; while (scope) { try { propDesc = Object.getOwnPropertyDescriptor(scope, prop); if (propDesc) { break; } } catch (ex) { // Native getters throw here. See bug 520882. if (ex.name === 'NS_ERROR_XPC_BAD_CONVERT_JS' || ex.name === 'NS_ERROR_XPC_BAD_OP_ON_WN_PROTO') { return false; } return true; } scope = Object.getPrototypeOf(scope); } if (!propDesc) { return false; } if (!propDesc.get) { return false; } // The property is safe if 'get' isn't a function or if the function has a // prototype (in which case it's native) return typeof propDesc.get !== 'function' || 'prototype' in propDesc.get; }; JavascriptType.prototype.name = 'javascript'; exports.JavascriptType = JavascriptType; }); /* * 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/types/node', ['require', 'exports', 'module' , 'gcli/host', 'gcli/l10n', 'gcli/types'], function(require, exports, module) { var host = require('gcli/host'); var l10n = require('gcli/l10n'); var types = require('gcli/types'); var Type = require('gcli/types').Type; var Status = require('gcli/types').Status; var Conversion = require('gcli/types').Conversion; /** * Registration and de-registration. */ exports.startup = function() { types.registerType(NodeType); }; exports.shutdown = function() { types.unregisterType(NodeType); }; /** * The object against which we complete, which is usually 'window' if it exists * but could be something else in non-web-content environments. */ var doc; if (typeof document !== 'undefined') { doc = document; } /** * Setter for the document that contains the nodes we're matching */ exports.setDocument = function(document) { doc = document; }; /** * Undo the effects of setDocument() */ exports.unsetDocument = function() { doc = undefined; }; /** * Getter for the document that contains the nodes we're matching * Most for changing things back to how they were for unit testing */ exports.getDocument = function() { return doc; }; /** * A CSS expression that refers to a single node */ function NodeType(typeSpec) { if (Object.keys(typeSpec).length > 0) { throw new Error('NodeType can not be customized'); } } NodeType.prototype = Object.create(Type.prototype); NodeType.prototype.stringify = function(value) { return value.__gcliQuery || 'Error'; }; NodeType.prototype.parse = function(arg) { if (arg.text === '') { return new Conversion(undefined, arg, Status.INCOMPLETE); } var nodes; try { nodes = doc.querySelectorAll(arg.text); } catch (ex) { return new Conversion(undefined, arg, Status.ERROR, l10n.lookup('nodeParseSyntax')); } if (nodes.length === 0) { return new Conversion(undefined, arg, Status.INCOMPLETE, l10n.lookup('nodeParseNone')); } if (nodes.length === 1) { var node = nodes.item(0); node.__gcliQuery = arg.text; host.flashNodes(node, true); return new Conversion(node, arg, Status.VALID, ''); } host.flashNodes(nodes, false); return new Conversion(undefined, arg, Status.ERROR, l10n.lookupFormat('nodeParseMultiple', [ nodes.length ])); }; NodeType.prototype.name = 'node'; }); /* * 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/host', ['require', 'exports', 'module' ], function(require, exports, module) { XPCOMUtils.defineLazyGetter(this, "Highlighter", function() { var tmp = {}; Components.utils.import("resource:///modules/highlighter.jsm", tmp); return tmp.Highlighter; }); /** * The chromeWindow as as required by Highlighter, so it knows where to * create temporary highlight nodes. */ exports.chromeWindow = undefined; /** * Helper to turn a set of nodes background another color for 0.5 seconds. * There is likely a better way to do this, but this will do for now. */ exports.flashNodes = function(nodes, match) { // Commented out until Bug 653545 is completed /* if (exports.chromeWindow == null) { console.log('flashNodes has no chromeWindow. Skipping flash'); } Highlighter.flashNodes(nodes, exports.chromeWindow, match); */ }; }); /* * 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/types/resource', ['require', 'exports', 'module' , 'gcli/types', 'gcli/types/selection'], function(require, exports, module) { var types = require('gcli/types'); var SelectionType = require('gcli/types/selection').SelectionType; /** * Registration and de-registration. */ exports.startup = function() { types.registerType(ResourceType); }; exports.shutdown = function() { types.unregisterType(ResourceType); exports.clearResourceCache(); }; exports.clearResourceCache = function() { ResourceCache.clear(); }; /** * The object against which we complete, which is usually 'window' if it exists * but could be something else in non-web-content environments. */ var doc; if (typeof document !== 'undefined') { doc = document; } /** * Setter for the document that contains the nodes we're matching */ exports.setDocument = function(document) { doc = document; }; /** * Undo the effects of setDocument() */ exports.unsetDocument = function() { ResourceCache.clear(); doc = undefined; }; /** * Getter for the document that contains the nodes we're matching * Most for changing things back to how they were for unit testing */ exports.getDocument = function() { return doc; }; /** * Resources are bits of CSS and JavaScript that the page either includes * directly or as a result of reading some remote resource. * Resource should not be used directly, but instead through a sub-class like * CssResource or ScriptResource. */ function Resource(name, type, inline, element) { this.name = name; this.type = type; this.inline = inline; this.element = element; } /** * Get the contents of the given resource as a string. * The base Resource leaves this unimplemented. */ Resource.prototype.getContents = function() { throw new Error('not implemented'); }; Resource.TYPE_SCRIPT = 'text/javascript'; Resource.TYPE_CSS = 'text/css'; /** * A CssResource provides an implementation of Resource that works for both * [style] elements and [link type='text/css'] elements in the [head]. */ function CssResource(domSheet) { this.name = domSheet.href; if (!this.name) { this.name = domSheet.ownerNode.id ? 'css#' + domSheet.ownerNode.id : 'inline-css'; } this.inline = (domSheet.href == null); this.type = Resource.TYPE_CSS; this.element = domSheet; } CssResource.prototype = Object.create(Resource.prototype); CssResource.prototype.loadContents = function(callback) { callback(this.element.ownerNode.innerHTML); }; CssResource._getAllStyles = function() { var resources = []; if (doc == null) { return resources; } Array.prototype.forEach.call(doc.styleSheets, function(domSheet) { CssResource._getStyle(domSheet, resources); }); dedupe(resources, function(clones) { for (var i = 0; i < clones.length; i++) { clones[i].name = clones[i].name + '-' + i; } }); return resources; }; CssResource._getStyle = function(domSheet, resources) { var resource = ResourceCache.get(domSheet); if (!resource) { resource = new CssResource(domSheet); ResourceCache.add(domSheet, resource); } resources.push(resource); // Look for imported stylesheets try { Array.prototype.forEach.call(domSheet.cssRules, function(domRule) { if (domRule.type == CSSRule.IMPORT_RULE && domRule.styleSheet) { CssResource._getStyle(domRule.styleSheet, resources); } }, this); } catch (ex) { // For system stylesheets } }; /** * A ScriptResource provides an implementation of Resource that works for * [script] elements (both with a src attribute, and used directly). */ function ScriptResource(scriptNode) { this.name = scriptNode.src; if (!this.name) { this.name = scriptNode.id ? 'script#' + scriptNode.id : 'inline-script'; } this.inline = (scriptNode.src === '' || scriptNode.src == null); this.type = Resource.TYPE_SCRIPT; this.element = scriptNode; } ScriptResource.prototype = Object.create(Resource.prototype); ScriptResource.prototype.loadContents = function(callback) { if (this.inline) { callback(this.element.innerHTML); } else { // It would be good if there was a better way to get the script source var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState !== xhr.DONE) { return; } callback(xhr.responseText); }; xhr.open('GET', this.element.src, true); xhr.send(); } }; ScriptResource._getAllScripts = function() { if (doc == null) { return []; } var scriptNodes = doc.querySelectorAll('script'); var resources = Array.prototype.map.call(scriptNodes, function(scriptNode) { var resource = ResourceCache.get(scriptNode); if (!resource) { resource = new ScriptResource(scriptNode); ResourceCache.add(scriptNode, resource); } return resource; }); dedupe(resources, function(clones) { for (var i = 0; i < clones.length; i++) { clones[i].name = clones[i].name + '-' + i; } }); return resources; }; /** * Find resources with the same name, and call onDupe to change the names */ function dedupe(resources, onDupe) { // first create a map of name->[array of resources with same name] var names = {}; resources.forEach(function(scriptResource) { if (names[scriptResource.name] == null) { names[scriptResource.name] = []; } names[scriptResource.name].push(scriptResource); }); // Call the de-dupe function for each set of dupes Object.keys(names).forEach(function(name) { var clones = names[name]; if (clones.length > 1) { onDupe(clones); } }); } /** * Use the Resource implementations to create a type based on SelectionType */ function ResourceType(typeSpec) { this.include = typeSpec.include; if (this.include !== Resource.TYPE_SCRIPT && this.include !== Resource.TYPE_CSS && this.include != null) { throw new Error('invalid include property: ' + this.include); } } ResourceType.prototype = Object.create(SelectionType.prototype); /** * There are several ways to get selection data. This unifies them into one * single function. * @return A map of names to values. */ ResourceType.prototype.getLookup = function() { var resources = []; if (this.include !== Resource.TYPE_SCRIPT) { Array.prototype.push.apply(resources, CssResource._getAllStyles()); } if (this.include !== Resource.TYPE_CSS) { Array.prototype.push.apply(resources, ScriptResource._getAllScripts()); } return resources.map(function(resource) { return { name: resource.name, value: resource }; }); }; ResourceType.prototype.name = 'resource'; /** * A quick cache of resources against nodes * TODO: Potential memory leak when the target document has css or script * resources repeatedly added and removed. Solution might be to use a weak * hash map or some such. */ var ResourceCache = { _cached: [], /** * Do we already have a resource that was created for the given node */ get: function(node) { for (var i = 0; i < ResourceCache._cached.length; i++) { if (ResourceCache._cached[i].node === node) { return ResourceCache._cached[i].resource; } } return null; }, /** * Add a resource for a given node */ add: function(node, resource) { ResourceCache._cached.push({ node: node, resource: resource }); }, /** * Drop all cache entries. Helpful to prevent memory leaks */ clear: function() { ResourceCache._cached = {}; } }; }); /* * 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/types/setting', ['require', 'exports', 'module' , 'gcli/settings', 'gcli/types', 'gcli/types/selection', 'gcli/types/basic'], function(require, exports, module) { var settings = require('gcli/settings'); var types = require('gcli/types'); var SelectionType = require('gcli/types/selection').SelectionType; var DeferredType = require('gcli/types/basic').DeferredType; /** * Registration and de-registration. */ exports.startup = function() { types.registerType(SettingType); types.registerType(SettingValueType); }; exports.shutdown = function() { types.unregisterType(SettingType); types.unregisterType(SettingValueType); }; /** * This is a whole new level of nasty. 'setting' and 'settingValue' are a pair * for obvious reasons. settingValue is a deferred type - it defers to the type * of the setting, but how do we implement the defer function - how does it * work out its paired setting? * In another parallel universe we pass the requisition to all the parse * methods so we can extract the args in SettingValueType.parse, however that * seems like a lot of churn for a simple way to connect 2 things. So we're * hacking. SettingType tries to keep 'lastSetting' up to date. */ var lastSetting = null; /** * A type for selecting a known setting */ function SettingType(typeSpec) { if (Object.keys(typeSpec).length > 0) { throw new Error('SettingType can not be customized'); } } SettingType.prototype = Object.create(SelectionType.prototype); SettingType.prototype.lookup = function() { return settings.getAll().map(function(setting) { return { name: setting.name, value: setting }; }); }; SettingType.prototype.noMatch = function() { lastSetting = null; }; SettingType.prototype.stringify = function(option) { lastSetting = option; return SelectionType.prototype.stringify.call(this, option); }; SettingType.prototype.parse = function(arg) { var conversion = SelectionType.prototype.parse.call(this, arg); lastSetting = conversion.value; return conversion; }; SettingType.prototype.name = 'setting'; /** * A type for entering the value of a known setting */ function SettingValueType(typeSpec) { if (Object.keys(typeSpec).length > 0) { throw new Error('SettingType can not be customized'); } } SettingValueType.prototype = Object.create(DeferredType.prototype); SettingValueType.prototype.defer = function() { if (lastSetting != null) { return lastSetting.type; } else { return types.getType('blank'); } }; SettingValueType.prototype.name = 'settingValue'; }); /* * 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/settings', ['require', 'exports', 'module' , 'gcli/util', 'gcli/types'], function(require, exports, module) { var imports = {}; Components.utils.import('resource://gre/modules/XPCOMUtils.jsm', imports); imports.XPCOMUtils.defineLazyGetter(imports, 'prefBranch', function() { var prefService = Components.classes['@mozilla.org/preferences-service;1'] .getService(Components.interfaces.nsIPrefService); return prefService.getBranch(null) .QueryInterface(Components.interfaces.nsIPrefBranch2); }); imports.XPCOMUtils.defineLazyGetter(imports, 'supportsString', function() { return Components.classes["@mozilla.org/supports-string;1"] .createInstance(Components.interfaces.nsISupportsString); }); var util = require('gcli/util'); var types = require('gcli/types'); var allSettings = []; /** * No setup required because settings are pre-loaded with Mozilla, * but match API with main settings.js */ exports.startup = function() { imports.prefBranch.getChildList('').forEach(function(name) { allSettings.push(new Setting(name)); }.bind(this)); allSettings.sort(function(s1, s2) { return s1.name.localeCompare(s2.name); }.bind(this)); }; exports.shutdown = function() { allSettings = []; }; /** * */ var DEVTOOLS_PREFIX = 'devtools.gcli.'; /** * A class to wrap up the properties of a preference. * @see toolkit/components/viewconfig/content/config.js */ function Setting(prefSpec) { if (typeof prefSpec === 'string') { // We're coming from getAll() i.e. a full listing of prefs this.name = prefSpec; this.description = ''; } else { // A specific addition by GCLI this.name = DEVTOOLS_PREFIX + prefSpec.name; if (prefSpec.ignoreTypeDifference !== true && prefSpec.type) { if (this.type.name !== prefSpec.type) { throw new Error('Locally declared type (' + prefSpec.type + ') != ' + 'Mozilla declared type (' + this.type.name + ') for ' + this.name); } } this.description = prefSpec.description; } this.onChange = util.createEvent('Setting.onChange'); } /** * What type is this property: boolean/integer/string? */ Object.defineProperty(Setting.prototype, 'type', { get: function() { switch (imports.prefBranch.getPrefType(this.name)) { case imports.prefBranch.PREF_BOOL: return types.getType('boolean'); case imports.prefBranch.PREF_INT: return types.getType('number'); case imports.prefBranch.PREF_STRING: return types.getType('string'); default: throw new Error('Unknown type for ' + this.name); } }, enumerable: true }); /** * What type is this property: boolean/integer/string? */ Object.defineProperty(Setting.prototype, 'value', { get: function() { switch (imports.prefBranch.getPrefType(this.name)) { case imports.prefBranch.PREF_BOOL: return imports.prefBranch.getBoolPref(this.name); case imports.prefBranch.PREF_INT: return imports.prefBranch.getIntPref(this.name); case imports.prefBranch.PREF_STRING: var value = imports.prefBranch.getComplexValue(this.name, Components.interfaces.nsISupportsString).data; // Try in case it's a localized string (will throw an exception if not) var isL10n = /^chrome:\/\/.+\/locale\/.+\.properties/.test(value); if (!this.changed && isL10n) { value = imports.prefBranch.getComplexValue(this.name, Components.interfaces.nsIPrefLocalizedString).data; } return value; default: throw new Error('Invalid value for ' + this.name); } }, set: function(value) { if (imports.prefBranch.prefIsLocked(this.name)) { throw new Error('Locked preference ' + this.name); } switch (imports.prefBranch.getPrefType(this.name)) { case imports.prefBranch.PREF_BOOL: imports.prefBranch.setBoolPref(this.name, value); break; case imports.prefBranch.PREF_INT: imports.prefBranch.setIntPref(this.name, value); break; case imports.prefBranch.PREF_STRING: imports.supportsString.data = value; imports.prefBranch.setComplexValue(this.name, Components.interfaces.nsISupportsString, imports.supportsString); break; default: throw new Error('Invalid value for ' + this.name); } Services.prefs.savePrefFile(null); }, enumerable: true }); /** * 'static' function to get an array containing all known Settings */ exports.getAll = function(filter) { if (filter == null) { return allSettings; } return allSettings.filter(function(setting) { return setting.name.indexOf(filter) !== -1; }); }; /** * Add a new setting. */ exports.addSetting = function(prefSpec) { var setting = new Setting(prefSpec); for (var i = 0; i < allSettings.length; i++) { if (allSettings[i].name === setting.name) { allSettings[i] = setting; } } return setting; }; /** * Remove a setting. A no-op in this case */ exports.removeSetting = function(nameOrSpec) { }; }); /* * 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/ui/intro', ['require', 'exports', 'module' , 'gcli/settings', 'gcli/l10n', 'gcli/ui/view', 'gcli/cli', 'text!gcli/ui/intro.html'], function(require, exports, module) { var settings = require('gcli/settings'); var l10n = require('gcli/l10n'); var view = require('gcli/ui/view'); var Output = require('gcli/cli').Output; /** * Record if the user has clicked on 'Got It!' */ var hideIntroSettingSpec = { name: 'hideIntro', type: 'boolean', description: l10n.lookup('hideIntroDesc'), defaultValue: false }; var hideIntro; /** * Register (and unregister) the hide-intro setting */ exports.startup = function() { hideIntro = settings.addSetting(hideIntroSettingSpec); }; exports.shutdown = function() { settings.removeSetting(hideIntroSettingSpec); hideIntro = undefined; }; /** * Called when the UI is ready to add a welcome message to the output */ exports.maybeShowIntro = function(commandOutputManager) { if (hideIntro.value) { return; } var output = new Output(); commandOutputManager.onOutput({ output: output }); var viewData = view.createView({ html: require('text!gcli/ui/intro.html'), data: { showHideButton: true, onGotIt: function(ev) { hideIntro.value = true; output.onClose(); } } }); output.complete(viewData); }; }); /* * 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/ui/view', ['require', 'exports', 'module' , 'gcli/util', 'gcli/ui/domtemplate'], function(require, exports, module) { var util = require('gcli/util'); var domtemplate = require('gcli/ui/domtemplate'); /** * We want to avoid commands having to create DOM structures because that's * messy and because we're going to need to have command output displayed in * different documents. A View is a way to wrap an HTML template (for * domtemplate) in with the data and options to render the template, so anyone * can later run the template in the context of any document. * View also cuts out a chunk of boiler place code. * @param options The information needed to create the DOM from HTML. Includes: * - html (required): The HTML source, probably from a call to require * - options (default={}): The domtemplate options. See domtemplate for details * - data (default={}): The data to domtemplate. See domtemplate for details. * - css (default=none): Some CSS to be added to the final document. If 'css' * is used, use of cssId is strongly recommended. * - cssId (default=none): An ID to prevent multiple CSS additions. See * util.importCss for more details. * @return An object containing a single function 'appendTo()' which runs the * template adding the result to the specified element. Takes 2 parameters: * - element (required): the element to add to * - clear (default=false): if clear===true then remove all pre-existing * children of 'element' before appending the results of this template. */ exports.createView = function(options) { if (options.html == null) { throw new Error('options.html is missing'); } return { /** * RTTI. Yeah. */ isView: true, /** * Run the template against the document to which element belongs. * @param element The element to append the result to * @param clear Set clear===true to remove all children of element */ appendTo: function(element, clear) { // Strict check on the off-chance that we later think of other options // and want to replace 'clear' with an 'options' parameter, but want to // support backwards compat. if (clear === true) { util.clearElement(element); } element.appendChild(this.toDom(element.ownerDocument)); }, /** * Actually convert the view data into a DOM suitable to be appended to * an element * @param document to use in realizing the template */ toDom: function(document) { if (options.css) { util.importCss(options.css, document, options.cssId); } var child = util.toDom(document, options.html); domtemplate.template(child, options.data || {}, options.options || {}); return child; } }; }; }); /* * 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/ui/domtemplate', ['require', 'exports', 'module' ], function(require, exports, module) { var obj = {}; Components.utils.import('resource:///modules/devtools/Templater.jsm', obj); exports.template = obj.template; }); /* * 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/cli', ['require', 'exports', 'module' , 'gcli/util', 'gcli/ui/view', 'gcli/canon', 'gcli/promise', 'gcli/types', 'gcli/types/basic', 'gcli/argument'], function(require, exports, module) { var util = require('gcli/util'); var view = require('gcli/ui/view'); var canon = require('gcli/canon'); var Promise = require('gcli/promise').Promise; var Status = require('gcli/types').Status; var Conversion = require('gcli/types').Conversion; var ArrayType = require('gcli/types/basic').ArrayType; var StringType = require('gcli/types/basic').StringType; var BooleanType = require('gcli/types/basic').BooleanType; var Argument = require('gcli/argument').Argument; var ArrayArgument = require('gcli/argument').ArrayArgument; var NamedArgument = require('gcli/argument').NamedArgument; var TrueNamedArgument = require('gcli/argument').TrueNamedArgument; var MergedArgument = require('gcli/argument').MergedArgument; var ScriptArgument = require('gcli/argument').ScriptArgument; var evalCommand; /** * Registration and de-registration. */ exports.startup = function() { evalCommand = canon.addCommand(evalCommandSpec); }; exports.shutdown = function() { canon.removeCommand(evalCommandSpec.name); evalCommand = undefined; }; /** * Assignment is a link between a parameter and the data for that parameter. * The data for the parameter is available as in the preferred type and as * an Argument for the CLI. *

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. * *

Events

* Assignment publishes the following event: