gecko/browser/devtools/webconsole/gcli.jsm

7701 lines
219 KiB
JavaScript

/*
* 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 Makefile.dryice.js for more details of this build.
* For more details on dryice, see the https://github.com/mozilla/dryice
*
*******************************************************************************
*
*
*
*
*
*
*
*
*
*/
///////////////////////////////////////////////////////////////////////////////
var EXPORTED_SYMBOLS = [ "gcli" ];
/**
* Expose a Node object. This allows us to use the Node constants without
* resorting to hardcoded numbers
*/
var Node = Components.interfaces.nsIDOMNode;
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) {
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 (type == "XULElement") {
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 (type == "XULElement") {
reply += " " + debugElement(aThing) + " (XUL)\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/javascript', 'gcli/types/node', 'gcli/cli', 'gcli/commands/help', 'gcli/ui/console'], 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/javascript').startup();
require('gcli/types/node').startup();
require('gcli/cli').startup();
require('gcli/commands/help').startup();
var Requisition = require('gcli/cli').Requisition;
var Console = require('gcli/ui/console').Console;
var cli = require('gcli/cli');
var jstype = require('gcli/types/javascript');
var nodetype = require('gcli/types/node');
/**
* 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
* - gcliTerm: GCLITerm
* - hintElement: GCLITerm.hintNode
* - inputBackgroundElement: GCLITerm.inputStack
*/
createView: function(opts) {
jstype.setGlobalObject(opts.jsEnvironment.globalObject);
nodetype.setDocument(opts.contentDocument);
cli.setEvalFunction(opts.jsEnvironment.evalFunction);
if (opts.requisition == null) {
opts.requisition = new Requisition(opts.environment, opts.chromeDocument);
}
opts.console = new Console(opts);
},
/**
* Undo the effects of createView() to prevent memory leaks
*/
removeView: function(opts) {
opts.console.destroy();
delete opts.console;
opts.requisition.destroy();
delete opts.requisition;
cli.unsetEvalFunction();
nodetype.unsetDocument();
jstype.unsetGlobalObject();
},
commandOutputManager: require('gcli/canon').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/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;
/**
* 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 = [];
/**
* 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;
// 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' };
Object.keys(paramSpec).forEach(function(key) {
this[key] = paramSpec[key];
}, this);
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);
this.groupName = groupName;
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 ('defaultValue' in this) {
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);
}
}
}
/**
* 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;
};
/**
* Is the user required to enter data for this parameter? (i.e. has
* defaultValue been set to something other than undefined)
*/
Parameter.prototype.isDataRequired = function() {
return this.defaultValue === undefined;
};
/**
* Are we allowed to assign data to this parameter using positional
* parameters?
*/
Parameter.prototype.isPositionalAllowed = function() {
return this.groupName == null;
};
canon.Parameter = Parameter;
/**
* 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) {
var command = new Command(commandSpec);
commands[commandSpec.name] = command;
commandNames.push(commandSpec.name);
commandNames.sort();
canon.canonChange();
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;
delete commands[name];
commandNames = commandNames.filter(function(test) {
return test !== name;
});
canon.canonChange();
};
/**
* 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);
};
/**
* Enable people to be notified of changes to the list of commands
*/
canon.canonChange = util.createEvent('canon.canonChange');
/**
* 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._event = util.createEvent('CommandOutputManager');
}
/**
* Call this method to notify the manager (and therefore all listeners) of a
* new or updated command output.
* @param output The command output object that has been created or updated.
*/
CommandOutputManager.prototype.sendCommandOutput = function(output) {
this._event({ output: output });
};
/**
* Register a function to be called whenever there is a new command output
* object.
*/
CommandOutputManager.prototype.addListener = function(fn, ctx) {
this._event.add(fn, ctx);
};
/**
* Undo the effects of CommandOutputManager.addListener()
*/
CommandOutputManager.prototype.removeListener = function(fn, ctx) {
this._event.remove(fn, ctx);
};
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.
*/
//------------------------------------------------------------------------------
/**
* Create an event.
* For use as follows:
*
* function Hat() {
* this.putOn = createEvent();
* ...
* }
* 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 = [];
/**
* This is how the event is triggered.
* @param ev The event object to be passed to the event listeners
*/
var event = function(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];
handler.func.call(handler.scope, ev);
}
};
/**
* 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) {
handlers = handlers.filter(function(test) {
return test.func !== func && test.scope !== scope;
});
};
/**
* Remove all handlers.
* Reset the state of this event back to it's post create state
*/
event.removeAll = function() {
handlers = [];
};
return event;
};
//------------------------------------------------------------------------------
var dom = {};
/**
* XHTML namespace
*/
dom.NS_XHTML = 'http://www.w3.org/1999/xhtml';
/**
* XUL namespace
*/
dom.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
*/
dom.createElement = function(doc, tag) {
if (dom.isXmlDocument(doc)) {
return doc.createElementNS(dom.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
*/
dom.clearElement = function(elem) {
while (elem.hasChildNodes()) {
elem.removeChild(elem.firstChild);
}
};
/**
* 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
*/
dom.importCss = function(cssText, doc) {
doc = doc || document;
var style = dom.createElement(doc, 'style');
style.appendChild(doc.createTextNode(cssText));
var head = doc.getElementsByTagName('head')[0] || doc.documentElement;
head.appendChild(style);
return style;
};
/**
* There are problems with innerHTML on XML documents, so we need to do a dance
* using document.createRange().createContextualFragment() when in XML mode
*/
dom.setInnerHtml = function(elem, html) {
if (dom.isXmlDocument(elem.ownerDocument)) {
try {
dom.clearElement(elem);
html = '<div xmlns="' + dom.NS_XHTML + '">' + html + '</div>';
var range = elem.ownerDocument.createRange();
var child = range.createContextualFragment(html).firstChild;
while (child.hasChildNodes()) {
elem.appendChild(child.firstChild);
}
}
catch (ex) {
console.error('Bad XHTML', ex);
console.trace();
throw ex;
}
}
else {
elem.innerHTML = html;
}
};
/**
* 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
*/
dom.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;
};
exports.dom = dom;
//------------------------------------------------------------------------------
/**
* Various event utilities
*/
var event = {};
/**
* 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) {
event.KeyEvent = this.KeyEvent;
}
else {
event.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
};
}
exports.event = event;
});
/*
* 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 {
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() {
var argProvided = this.arg.text !== '';
return this.value !== undefined || argProvided;
};
/**
* 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 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 || [];
};
/**
* 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 <tt>value</tt> 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 <tt>arg</tt> to an instance of this type.
* Where possible, there should be round-tripping between values and their
* string representations.
* @param arg An instance of <tt>Argument</tt> 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;
};
/**
* There is interesting information (like predictions) in a conversion of
* nothing, the output of this can sometimes be customized.
* @return Conversion
*/
Type.prototype.getDefault = undefined;
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
* <tt>replText</tt> 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;
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.
*/
function ScriptArgument(text, prefix, suffix) {
this.text = 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
* <tt>replText</tt> 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;
var quote = (replText.indexOf(' ') >= 0 || replText.length == 0) ?
'\'' : '';
if (options && options.normalize) {
prefix = '{ ';
suffix = ' }';
}
return new ScriptArgument(replText, prefix, suffix);
};
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:
* <ul>
* <li>--param value
* <li>-p value
* <li>--pa value
* <li>-p:value
* <li>--param=value
* <li>etc
* </ul>
* 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/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 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(SelectionType);
types.registerType(DeferredType);
types.registerType(ArrayType);
};
exports.shutdown = function() {
types.unregisterType(StringType);
types.unregisterType(NumberType);
types.unregisterType(BooleanType);
types.unregisterType(BlankType);
types.unregisterType(SelectionType);
types.unregisterType(DeferredType);
types.unregisterType(ArrayType);
};
/**
* 'string' the most basic string type that doesn't need to convert
*/
function StringType(typeSpec) {
if (typeSpec != null) {
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(null, 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 0;
};
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(null, arg, Status.INCOMPLETE, '');
}
var value = parseInt(arg.text, 10);
if (isNaN(value)) {
return new Conversion(null, arg, Status.ERROR,
l10n.lookupFormat('typesNumberNan', [ arg.text ]));
}
if (this.getMax() != null && value > this.getMax()) {
return new Conversion(null, arg, Status.ERROR,
l10n.lookupFormat('typesNumberMax', [ value, this.getMax() ]));
}
if (value < this.getMin()) {
return new Conversion(null, arg, Status.ERROR,
l10n.lookupFormat('typesNumberMin', [ value, this.getMin() ]));
}
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)) {
return this.getMin();
}
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 (value < min) {
return min;
}
var max = this.getMax();
if (value > max) {
return max;
}
return value;
};
NumberType.prototype.name = 'number';
exports.NumberType = NumberType;
/**
* One of a known set of options
*/
function SelectionType(typeSpec) {
if (typeSpec) {
Object.keys(typeSpec).forEach(function(key) {
this[key] = typeSpec[key];
}, this);
}
}
SelectionType.prototype = Object.create(Type.prototype);
SelectionType.prototype.stringify = function(value) {
var name = null;
var lookup = this.getLookup();
lookup.some(function(item) {
var test = (item.value == null) ? item : item.value;
if (test === 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 A map of names to values.
*/
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 };
});
};
/**
* Return a list of possible completions for the given arg.
* This code is very similar to CommandType._findPredictions(). If you are
* making changes to this code, you should check there too.
* @param arg The initial input to match
* @return A trimmed lookup table of string:value pairs
*/
SelectionType.prototype._findPredictions = function(arg) {
var predictions = [];
this.getLookup().forEach(function(item) {
if (item.name.indexOf(arg.text) === 0) {
predictions.push(item);
}
}, this);
return predictions;
};
SelectionType.prototype.parse = function(arg) {
var predictions = this._findPredictions(arg);
if (predictions.length === 1 && predictions[0].name === arg.text) {
var value = predictions[0].value ? predictions[0].value : predictions[0];
return new Conversion(value, arg);
}
// 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();
}
if (predictions.length > 0) {
// Especially at startup, predictions live over the time that things
// change so we provide a completion function rather than completion
// values.
// This was primarily designed for commands, which have since moved
// into their own type, so technically we could remove this code,
// except that it provides more up-to-date answers, and it's hard to
// predict when it will be required.
var predictFunc = function() {
return this._findPredictions(arg);
}.bind(this);
return new Conversion(null, arg, Status.INCOMPLETE, '', predictFunc);
}
return new Conversion(null, arg, Status.ERROR,
l10n.lookupFormat('typesSelectionNomatch', [ arg.text ]));
};
/**
* 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;
/**
* true/false values
*/
function BooleanType(typeSpec) {
if (typeSpec != null) {
throw new Error('BooleanType can not be customized');
}
}
BooleanType.prototype = Object.create(SelectionType.prototype);
BooleanType.prototype.lookup = [
{ name: 'true', value: true },
{ name: 'false', value: false }
];
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.getDefault = function() {
return new Conversion(false, new Argument(''));
};
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.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 (typeSpec != null) {
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(null, 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.getDefault = function() {
return new ArrayConversion([], new ArrayArgument());
};
ArrayType.prototype.name = 'array';
exports.ArrayType = ArrayType;
});
/*
* 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 (typeSpec != null) {
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;
// 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 (typeSpec != null) {
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(null, arg, Status.INCOMPLETE,
l10n.lookup('nodeParseNone'));
}
var nodes;
try {
nodes = doc.querySelectorAll(arg.text);
}
catch (ex) {
return new Conversion(null, arg, Status.ERROR,
l10n.lookup('nodeParseSyntax'));
}
if (nodes.length === 0) {
return new Conversion(null, arg, Status.INCOMPLETE,
l10n.lookup('nodeParseNone'));
}
if (nodes.length === 1) {
var node = nodes.item(0);
node.__gcliQuery = arg.text;
host.flashNode(node, 'green');
return new Conversion(node, arg, Status.VALID, '');
}
Array.prototype.forEach.call(nodes, function(n) {
host.flashNode(n, 'red');
});
return new Conversion(null, 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) {
/**
* Helper to turn a node background it's complementary color for 1 second.
* There is likely a better way to do this, but this will do for now.
*/
exports.flashNode = function(node, color) {
if (!node.__gcliHighlighting) {
node.__gcliHighlighting = true;
var original = node.style.background;
node.style.background = color;
setTimeout(function() {
node.style.background = original;
delete node.__gcliHighlighting;
}, 1000);
}
};
});
/*
* 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/canon', 'gcli/promise', 'gcli/types', 'gcli/types/basic', 'gcli/argument'], function(require, exports, module) {
var util = require('gcli/util');
var canon = require('gcli/canon');
var Promise = require('gcli/promise').Promise;
var types = require('gcli/types');
var Status = require('gcli/types').Status;
var Conversion = require('gcli/types').Conversion;
var Type = require('gcli/types').Type;
var ArrayType = require('gcli/types/basic').ArrayType;
var StringType = require('gcli/types/basic').StringType;
var BooleanType = require('gcli/types/basic').BooleanType;
var SelectionType = require('gcli/types/basic').SelectionType;
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() {
types.registerType(CommandType);
evalCommand = canon.addCommand(evalCommandSpec);
};
exports.shutdown = function() {
types.unregisterType(CommandType);
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.
* <p>We also record validity information where applicable.
* <p>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.
*
* <h2>Events<h2>
* Assignment publishes the following event:<ul>
* <li>assignmentChange: Either the value or the text has changed. It is likely
* that any UI component displaying this argument will need to be updated.
* The event object looks like:
* <tt>{ assignment: ..., conversion: ..., oldConversion: ... }</tt>
* @constructor
*/
function Assignment(param, paramIndex) {
this.param = param;
this.paramIndex = paramIndex;
this.assignmentChange = util.createEvent('Assignment.assignmentChange');
this.setDefault();
}
/**
* The parameter that we are assigning to
* @readonly
*/
Assignment.prototype.param = undefined;
Assignment.prototype.conversion = undefined;
/**
* The index of this parameter in the parent Requisition. paramIndex === -1
* is the command assignment although this should not be relied upon, it is
* better to test param instanceof CommandAssignment
*/
Assignment.prototype.paramIndex = undefined;
/**
* Easy accessor for conversion.arg
*/
Assignment.prototype.getArg = function() {
return this.conversion.arg;
};
/**
* Easy accessor for conversion.value
*/
Assignment.prototype.getValue = function() {
return this.conversion.value;
};
/**
* Easy (and safe) accessor for conversion.message
*/
Assignment.prototype.getMessage = function() {
return this.conversion.message ? this.conversion.message : '';
};
/**
* Easy (and safe) accessor for conversion.getPredictions()
* @return An array of objects with name and value elements. For example:
* [ { name:'bestmatch', value:foo1 }, { name:'next', value:foo2 }, ... ]
*/
Assignment.prototype.getPredictions = function() {
return this.conversion.getPredictions();
};
/**
* Report on the status of the last parse() conversion.
* We force mutations to happen through this method rather than have
* setValue and setArgument functions to help maintain integrity when we
* have ArrayArguments and don't want to get confused. This way assignments
* are just containers for a conversion rather than things that store
* a connection between an arg/value.
* @see types.Conversion
*/
Assignment.prototype.setConversion = function(conversion) {
var oldConversion = this.conversion;
this.conversion = conversion;
this.conversion.assign(this);
if (this.conversion.equals(oldConversion)) {
return;
}
this.assignmentChange({
assignment: this,
conversion: this.conversion,
oldConversion: oldConversion
});
};
/**
* Find a default value for the conversion either from the parameter, or from
* the type, or failing that by parsing an empty argument.
*/
Assignment.prototype.setDefault = function() {
var conversion;
if (this.param.getDefault) {
conversion = this.param.getDefault();
}
else if (this.param.type.getDefault) {
conversion = this.param.type.getDefault();
}
else {
conversion = this.param.type.parse(new Argument());
}
this.setConversion(conversion);
};
/**
* Make sure that there is some content for this argument by using an
* Argument of '' if needed.
*/
Assignment.prototype.ensureVisibleArgument = function() {
// It isn't clear if we should be sending events from this method.
// It should only be called when structural changes are happening in which
// case we're going to ignore the event anyway. But on the other hand
// perhaps this function shouldn't need to know how it is used, and should
// do the inefficient thing.
if (!this.conversion.arg.isBlank()) {
return false;
}
var arg = this.conversion.arg.beget('', {
prefixSpace: this.param instanceof CommandAssignment
});
this.conversion = this.param.type.parse(arg);
this.conversion.assign(this);
return true;
};
/**
* Work out what the status of the current conversion is which involves looking
* not only at the conversion, but also checking if data has been provided
* where it should.
* @param arg For assignments with multiple args (e.g. array assignments) we
* can narrow the search for status to a single argument.
*/
Assignment.prototype.getStatus = function(arg) {
if (this.param.isDataRequired() && !this.conversion.isDataProvided()) {
return Status.ERROR;
}
// Selection/Boolean types with a defined range of values will say that
// '' is INCOMPLETE, but the parameter may be optional, so we don't ask
// if the user doesn't need to enter something and hasn't done so.
if (!this.param.isDataRequired() && this.getArg().isBlank()) {
return Status.VALID;
}
return this.conversion.getStatus(arg);
};
/**
* Basically <tt>value = conversion.predictions[0])</tt> done in a safe way.
*/
Assignment.prototype.complete = function() {
var predictions = this.conversion.getPredictions();
if (predictions.length > 0) {
var arg = this.conversion.arg.beget(predictions[0].name);
if (!predictions[0].incomplete) {
arg.suffix = ' ';
}
var conversion = this.param.type.parse(arg);
this.setConversion(conversion);
}
};
/**
* Replace the current value with the lower value if such a concept exists.
*/
Assignment.prototype.decrement = function() {
var replacement = this.param.type.decrement(this.conversion.value);
if (replacement != null) {
var str = this.param.type.stringify(replacement);
var arg = this.conversion.arg.beget(str);
var conversion = new Conversion(replacement, arg);
this.setConversion(conversion);
}
};
/**
* Replace the current value with the higher value if such a concept exists.
*/
Assignment.prototype.increment = function() {
var replacement = this.param.type.increment(this.conversion.value);
if (replacement != null) {
var str = this.param.type.stringify(replacement);
var arg = this.conversion.arg.beget(str);
var conversion = new Conversion(replacement, arg);
this.setConversion(conversion);
}
};
/**
* Helper when we're rebuilding command lines.
*/
Assignment.prototype.toString = function() {
return this.conversion.toString();
};
exports.Assignment = Assignment;
/**
* 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() {
}
CommandType.prototype = Object.create(Type.prototype);
CommandType.prototype.name = 'command';
CommandType.prototype.decrement = SelectionType.prototype.decrement;
CommandType.prototype.increment = SelectionType.prototype.increment;
CommandType.prototype._findValue = SelectionType.prototype._findValue;
CommandType.prototype.stringify = function(command) {
return command.name;
};
/**
* Trim a list of commands (as from canon.getCommands()) according to those
* that match the provided arg.
*/
CommandType.prototype._findPredictions = function(arg) {
var predictions = [];
canon.getCommands().forEach(function(command) {
if (command.name.indexOf(arg.text) === 0) {
// 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 || command.name.indexOf(' ') === -1) {
predictions.push(command);
}
}
}, this);
return predictions;
};
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) {
return new Conversion(null, arg, Status.ERROR,
'Can\'t use \'' + arg.text + '\'.', predictFunc);
}
var command = predictions[0];
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(null, arg, Status.INCOMPLETE, '', predictFunc);
}
// It's valid if the text matches, even if there are several options
if (command.name === arg.text) {
return new Conversion(command, arg, Status.VALID, '', predictFunc);
}
return new Conversion(null, arg, Status.INCOMPLETE, '', predictFunc);
};
/**
* How to dynamically execute JavaScript code
*/
var customEval = eval;
/**
* Setup a function to be called in place of 'eval', generally for security
* reasons
*/
exports.setEvalFunction = function(newCustomEval) {
customEval = newCustomEval;
};
/**
* Remove the binding done by setEvalFunction().
* We purposely set customEval to undefined rather than to 'eval' because there
* is an implication of setEvalFunction that we're in a security sensitive
* situation. What if we can trick GCLI into calling unsetEvalFunction() at the
* wrong time?
* So to properly undo the effects of setEvalFunction(), you need to call
* setEvalFunction(eval) rather than unsetEvalFunction(), however the latter is
* preferred in most cases.
*/
exports.unsetEvalFunction = function() {
customEval = undefined;
};
/**
* 'eval' command
*/
var evalCommandSpec = {
name: '{',
params: [
{
name: 'javascript',
type: 'javascript',
description: ''
}
],
returnType: 'html',
description: { key: 'cliEvalJavascript' },
exec: function(args, context) {
// &#x2192; is right arrow. We use explicit entities to ensure XML validity
var resultPrefix = '<em>{ ' + args.javascript + ' }</em> &#x2192; ';
try {
var result = customEval(args.javascript);
if (result === null) {
return resultPrefix + 'null.';
}
if (result === undefined) {
return resultPrefix + 'undefined.';
}
if (typeof result === 'function') {
// &#160; is &nbsp;
return resultPrefix +
(result + '').replace(/\n/g, '<br>').replace(/ /g, '&#160;');
}
return resultPrefix + result;
}
catch (ex) {
return resultPrefix + 'Exception: ' + ex.message;
}
}
};
/**
* This is a special assignment to reflect the command itself.
*/
function CommandAssignment() {
this.param = new canon.Parameter({
name: '__command',
type: 'command',
description: 'The command to execute'
});
this.paramIndex = -1;
this.assignmentChange = util.createEvent('CommandAssignment.assignmentChange');
this.setDefault();
}
CommandAssignment.prototype = Object.create(Assignment.prototype);
CommandAssignment.prototype.getStatus = function(arg) {
return Status.combine(
Assignment.prototype.getStatus.call(this, arg),
this.conversion.value && !this.conversion.value.exec ?
Status.INCOMPLETE : Status.VALID
);
};
/**
* Special assignment used when ignoring parameters that don't have a home
*/
function UnassignedAssignment() {
this.param = new canon.Parameter({
name: '__unassigned',
type: 'string'
});
this.paramIndex = -1;
this.assignmentChange = util.createEvent('UnassignedAssignment.assignmentChange');
this.setDefault();
}
UnassignedAssignment.prototype = Object.create(Assignment.prototype);
UnassignedAssignment.prototype.getStatus = function(arg) {
return Status.ERROR;
};
UnassignedAssignment.prototype.setUnassigned = function(args) {
if (!args || args.length === 0) {
this.setDefault();
}
else {
var conversion = this.param.type.parse(new MergedArgument(args));
this.setConversion(conversion);
}
};
/**
* A Requisition collects the information needed to execute a command.
*
* (For a definition of the term, see http://en.wikipedia.org/wiki/Requisition)
* This term is used because carries the notion of a work-flow, or process to
* getting the information to execute a command correct.
* There is little point in a requisition for parameter-less commands because
* there is no information to collect. A Requisition is a collection of
* assignments of values to parameters, each handled by an instance of
* Assignment.
*
* <h2>Events<h2>
* <p>Requisition publishes the following events:
* <ul>
* <li>commandChange: The command has changed. It is likely that a UI
* structure will need updating to match the parameters of the new command.
* The event object looks like { command: A }
* <li>assignmentChange: This is a forward of the Assignment.assignmentChange
* event. It is fired when any assignment (except the commandAssignment)
* changes.
* <li>inputChange: The text to be mirrored in a command line has changed.
* The event object looks like { newText: X }.
* </ul>
*
* @param environment An optional opaque object passed to commands using
* ExecutionContext.
* @param doc A DOM Document passed to commands using ExecutionContext in
* order to allow creation of DOM nodes. If missing Requisition will use the
* global 'document'.
* @constructor
*/
function Requisition(environment, doc) {
this.environment = environment;
this.document = doc;
if (this.document == null) {
try {
this.document = document;
}
catch (ex) {
// Ignore
}
}
// The command that we are about to execute.
// @see setCommandConversion()
this.commandAssignment = new CommandAssignment();
// The object that stores of Assignment objects that we are filling out.
// The Assignment objects are stored under their param.name for named
// lookup. Note: We make use of the property of Javascript objects that
// they are not just hashmaps, but linked-list hashmaps which iterate in
// insertion order.
// _assignments excludes the commandAssignment.
this._assignments = {};
// The count of assignments. Excludes the commandAssignment
this.assignmentCount = 0;
// Used to store cli arguments in the order entered on the cli
this._args = [];
// Used to store cli arguments that were not assigned to parameters
this._unassigned = new UnassignedAssignment();
// Temporarily set this to true to prevent _onAssignmentChange resetting
// argument positions
this._structuralChangeInProgress = false;
this.commandAssignment.assignmentChange.add(this._onCommandAssignmentChange, this);
this.commandAssignment.assignmentChange.add(this._onAssignmentChange, this);
this.commandOutputManager = canon.commandOutputManager;
this.assignmentChange = util.createEvent('Requisition.assignmentChange');
this.commandChange = util.createEvent('Requisition.commandChange');
this.inputChange = util.createEvent('Requisition.inputChange');
}
/**
* Some number that is higher than the most args we'll ever have. Would use
* MAX_INTEGER if that made sense
*/
var MORE_THAN_THE_MOST_ARGS_POSSIBLE = 1000000;
/**
* Avoid memory leaks
*/
Requisition.prototype.destroy = function() {
this.commandAssignment.assignmentChange.remove(this._onCommandAssignmentChange, this);
this.commandAssignment.assignmentChange.remove(this._onAssignmentChange, this);
delete this.document;
delete this.environment;
};
/**
* When any assignment changes, we might need to update the _args array to
* match and inform people of changes to the typed input text.
*/
Requisition.prototype._onAssignmentChange = function(ev) {
// Don't report an event if the value is unchanged
if (ev.oldConversion != null &&
ev.conversion.valueEquals(ev.oldConversion)) {
return;
}
if (this._structuralChangeInProgress) {
return;
}
this.assignmentChange(ev);
// Both for argument position and the inputChange event, we only care
// about changes to the argument.
if (ev.conversion.argEquals(ev.oldConversion)) {
return;
}
this._structuralChangeInProgress = true;
// Refactor? See bug 660765
// Do preceding arguments need to have dummy values applied so we don't
// get a hole in the command line?
if (ev.assignment.param.isPositionalAllowed()) {
for (var i = 0; i < ev.assignment.paramIndex; i++) {
var assignment = this.getAssignment(i);
if (assignment.param.isPositionalAllowed()) {
if (assignment.ensureVisibleArgument()) {
this._args.push(assignment.getArg());
}
}
}
}
// Remember where we found the first match
var index = MORE_THAN_THE_MOST_ARGS_POSSIBLE;
for (var i = 0; i < this._args.length; i++) {
if (this._args[i].assignment === ev.assignment) {
if (i < index) {
index = i;
}
this._args.splice(i, 1);
i--;
}
}
if (index === MORE_THAN_THE_MOST_ARGS_POSSIBLE) {
this._args.push(ev.assignment.getArg());
}
else {
// Is there a way to do this that doesn't involve a loop?
var newArgs = ev.conversion.arg.getArgs();
for (var i = 0; i < newArgs.length; i++) {
this._args.splice(index + i, 0, newArgs[i]);
}
}
this._structuralChangeInProgress = false;
this.inputChange();
};
/**
* When the command changes, we need to keep a bunch of stuff in sync
*/
Requisition.prototype._onCommandAssignmentChange = function(ev) {
this._assignments = {};
var command = this.commandAssignment.getValue();
if (command) {
for (var i = 0; i < command.params.length; i++) {
var param = command.params[i];
var assignment = new Assignment(param, i);
assignment.assignmentChange.add(this._onAssignmentChange, this);
this._assignments[param.name] = assignment;
}
}
this.assignmentCount = Object.keys(this._assignments).length;
this.commandChange({
requisition: this,
oldValue: ev.oldValue,
newValue: command
});
};
/**
* Assignments have an order, so we need to store them in an array.
* But we also need named access ...
* @return The found assignment, or undefined, if no match was found
*/
Requisition.prototype.getAssignment = function(nameOrNumber) {
var name = (typeof nameOrNumber === 'string') ?
nameOrNumber :
Object.keys(this._assignments)[nameOrNumber];
return this._assignments[name] || undefined;
},
/**
* Where parameter name == assignment names - they are the same
*/
Requisition.prototype.getParameterNames = function() {
return Object.keys(this._assignments);
},
/**
* A *shallow* clone of the assignments.
* This is useful for systems that wish to go over all the assignments
* finding values one way or another and wish to trim an array as they go.
*/
Requisition.prototype.cloneAssignments = function() {
return Object.keys(this._assignments).map(function(name) {
return this._assignments[name];
}, this);
};
/**
* The overall status is the most severe status.
* There is no such thing as an INCOMPLETE overall status because the
* definition of INCOMPLETE takes into account the cursor position to say 'this
* isn't quite ERROR because the user can fix it by typing', however overall,
* this is still an error status.
*/
Requisition.prototype.getStatus = function() {
var status = Status.VALID;
this.getAssignments(true).forEach(function(assignment) {
var assignStatus = assignment.getStatus();
if (assignment.getStatus() > status) {
status = assignStatus;
}
}, this);
if (status === Status.INCOMPLETE) {
status = Status.ERROR;
}
return status;
};
/**
* Extract the names and values of all the assignments, and return as
* an object.
*/
Requisition.prototype.getArgsObject = function() {
var args = {};
this.getAssignments().forEach(function(assignment) {
args[assignment.param.name] = assignment.getValue();
}, this);
return args;
};
/**
* Access the arguments as an array.
* @param includeCommand By default only the parameter arguments are
* returned unless (includeCommand === true), in which case the list is
* prepended with commandAssignment.getArg()
*/
Requisition.prototype.getAssignments = function(includeCommand) {
var assignments = [];
if (includeCommand === true) {
assignments.push(this.commandAssignment);
}
Object.keys(this._assignments).forEach(function(name) {
assignments.push(this.getAssignment(name));
}, this);
return assignments;
};
/**
* Reset all the assignments to their default values
*/
Requisition.prototype.setDefaultArguments = function() {
this.getAssignments().forEach(function(assignment) {
assignment.setDefault();
}, this);
};
/**
* Extract a canonical version of the input
*/
Requisition.prototype.toCanonicalString = function() {
var line = [];
var cmd = this.commandAssignment.getValue() ?
this.commandAssignment.getValue().name :
this.commandAssignment.getArg().text;
line.push(cmd);
Object.keys(this._assignments).forEach(function(name) {
var assignment = this._assignments[name];
var type = assignment.param.type;
// Bug 664377: This will cause problems if there is a non-default value
// after a default value. Also we need to decide when to use
// named parameters in place of positional params. Both can wait.
if (assignment.getValue() !== assignment.param.defaultValue) {
line.push(' ');
line.push(type.stringify(assignment.getValue()));
}
}, this);
// Canonically, if we've opened with a { then we should have a } to close
if (cmd === '{') {
if (this.getAssignment(0).getArg().suffix.indexOf('}') === -1) {
line.push(' }');
}
}
return line.join('');
};
/**
* Input trace gives us an array of Argument tracing objects, one for each
* character in the typed input, from which we can derive information about how
* to display this typed input. It's a bit like toString on steroids.
* <p>
* The returned object has the following members:<ul>
* <li>char: The character to which this arg trace refers.
* <li>arg: The Argument to which this character is assigned.
* <li>part: One of ['prefix'|'text'|suffix'] - how was this char understood
* </ul>
* <p>
* The Argument objects are as output from #_tokenize() rather than as applied
* to Assignments by #_assign() (i.e. they are not instances of NamedArgument,
* ArrayArgument, etc).
* <p>
* To get at the arguments applied to the assignments simply call
* <tt>arg.assignment.arg</tt>. If <tt>arg.assignment.arg !== arg</tt> then
* the arg applied to the assignment will contain the original arg.
* See #_assign() for details.
*/
Requisition.prototype.createInputArgTrace = function() {
if (!this._args) {
throw new Error('createInputMap requires a command line. See source.');
// If this is a problem then we can fake command line input using
// something like the code in #toCanonicalString().
}
var args = [];
this._args.forEach(function(arg) {
for (var i = 0; i < arg.prefix.length; i++) {
args.push({ arg: arg, char: arg.prefix[i], part: 'prefix' });
}
for (var i = 0; i < arg.text.length; i++) {
args.push({ arg: arg, char: arg.text[i], part: 'text' });
}
for (var i = 0; i < arg.suffix.length; i++) {
args.push({ arg: arg, char: arg.suffix[i], part: 'suffix' });
}
});
return args;
};
/**
* Reconstitute the input from the args
*/
Requisition.prototype.toString = function() {
if (this._args) {
return this._args.map(function(arg) {
return arg.toString();
}).join('');
}
return this.toCanonicalString();
};
/**
* Return an array of Status scores so we can create a marked up
* version of the command line input.
* @param cursor We only take a status of INCOMPLETE to be INCOMPLETE when the
* cursor is actually in the argument. Otherwise it's an error.
*/
Requisition.prototype.getInputStatusMarkup = function(cursor) {
var argTraces = this.createInputArgTrace();
// Generally the 'argument at the cursor' is the argument before the cursor
// unless it is before the first char, in which case we take the first.
cursor = cursor === 0 ? 0 : cursor - 1;
var cTrace = argTraces[cursor];
var statuses = [];
for (var i = 0; i < argTraces.length; i++) {
var argTrace = argTraces[i];
var arg = argTrace.arg;
var status = Status.VALID;
if (argTrace.part === 'text') {
status = arg.assignment.getStatus(arg);
// Promote INCOMPLETE to ERROR ...
if (status === Status.INCOMPLETE) {
// If the cursor is not in a position to be able to complete it
if (arg !== cTrace.arg || cTrace.part !== 'text') {
// And if we're not in the command
if (!(arg.assignment instanceof CommandAssignment)) {
status = Status.ERROR;
}
}
}
}
statuses.push(status);
}
return statuses;
};
/**
* Look through the arguments attached to our assignments for the assignment
* at the given position.
* @param {number} cursor The cursor position to query
*/
Requisition.prototype.getAssignmentAt = function(cursor) {
if (!this._args) {
console.trace();
throw new Error('Missing args');
}
// We short circuit this one because we may have no args, or no args with
// any size and the alg below only finds arguments with size.
if (cursor === 0) {
return this.commandAssignment;
}
var assignForPos = [];
var i, j;
for (i = 0; i < this._args.length; i++) {
var arg = this._args[i];
var assignment = arg.assignment;
// prefix and text are clearly part of the argument
for (j = 0; j < arg.prefix.length; j++) {
assignForPos.push(assignment);
}
for (j = 0; j < arg.text.length; j++) {
assignForPos.push(assignment);
}
// suffix looks forwards
if (this._args.length > i + 1) {
// first to the next argument
assignment = this._args[i + 1].assignment;
}
else if (assignment &&
assignment.paramIndex + 1 < this.assignmentCount) {
// then to the next assignment
assignment = this.getAssignment(assignment.paramIndex + 1);
}
for (j = 0; j < arg.suffix.length; j++) {
assignForPos.push(assignment);
}
}
// Possible shortcut, we don't really need to go through all the args
// to work out the solution to this
var reply = assignForPos[cursor - 1];
if (!reply) {
throw new Error('Missing assignment.' +
' cursor=' + cursor + ' text.length=' + this.toString().length);
}
return reply;
};
/**
* Entry point for keyboard accelerators or anything else that wants to execute
* a command.
* @param input Object containing data about how to execute the command.
* Properties of input include:
* - args: Arguments for the command
* - typed: The typed command
* - visible: Ensure that the output from this command is visible
*/
Requisition.prototype.exec = function(input) {
var command;
var args;
var visible = true;
if (input) {
if (input.args != null) {
// Fast track by looking up the command directly since passed args
// means there is no command line to parse.
command = canon.getCommand(input.typed);
if (!command) {
console.error('Command not found: ' + command);
}
args = input.args;
// Default visible to false since this is exec is probably the
// result of a keyboard shortcut
visible = 'visible' in input ? input.visible : false;
}
else {
this.update(input);
}
}
if (!command) {
command = this.commandAssignment.getValue();
args = this.getArgsObject();
}
if (!command) {
return false;
}
var outputObject = {
command: command,
args: args,
typed: this.toString(),
canonical: this.toCanonicalString(),
completed: false,
start: new Date()
};
this.commandOutputManager.sendCommandOutput(outputObject);
var onComplete = (function(output, error) {
if (visible) {
outputObject.end = new Date();
outputObject.duration = outputObject.end.getTime() - outputObject.start.getTime();
outputObject.error = error;
outputObject.output = output;
outputObject.completed = true;
this.commandOutputManager.sendCommandOutput(outputObject);
}
}).bind(this);
try {
var context = new ExecutionContext(this);
var reply = command.exec(args, context);
if (reply != null && reply.isPromise) {
reply.then(
function(data) { onComplete(data, false); },
function(error) { onComplete(error, true); });
// Add progress to our promise and add a handler for it here
// See bug 659300
}
else {
onComplete(reply, false);
}
}
catch (ex) {
onComplete(ex, true);
}
this.clear();
return true;
};
/**
* Called by the UI when ever the user interacts with a command line input
* @param input A structure that details the state of the input field.
* It should look something like: { typed:a, cursor: { start:b, end:c } }
* Where a is the contents of the input field, and b and c are the start
* and end of the cursor/selection respectively.
* <p>The general sequence is:
* <ul>
* <li>_tokenize(): convert _typed into _parts
* <li>_split(): convert _parts into _command and _unparsedArgs
* <li>_assign(): convert _unparsedArgs into requisition
* </ul>
*/
Requisition.prototype.update = function(input) {
if (input.cursor == null) {
input.cursor = { start: input.length, end: input.length };
}
this._structuralChangeInProgress = true;
this._args = this._tokenize(input.typed);
var args = this._args.slice(0); // i.e. clone
this._split(args);
this._assign(args);
this._structuralChangeInProgress = false;
this.inputChange();
};
/**
* Empty the current buffer, and notify listeners that we're now empty
*/
Requisition.prototype.clear = function() {
this.update({ typed: '', cursor: { start: 0, end: 0 } });
};
/**
* Requisition._tokenize() is a state machine. These are the states.
*/
var In = {
/**
* The last character was ' '.
* Typing a ' ' character will not change the mode
* Typing one of '"{ will change mode to SINGLE_Q, DOUBLE_Q or SCRIPT.
* Anything else goes into SIMPLE mode.
*/
WHITESPACE: 1,
/**
* The last character was part of a parameter.
* Typing ' ' returns to WHITESPACE mode. Any other character
* (including '"{} which are otherwise special) does not change the mode.
*/
SIMPLE: 2,
/**
* We're inside single quotes: '
* Typing ' returns to WHITESPACE mode. Other characters do not change mode.
*/
SINGLE_Q: 3,
/**
* We're inside double quotes: "
* Typing " returns to WHITESPACE mode. Other characters do not change mode.
*/
DOUBLE_Q: 4,
/**
* We're inside { and }
* Typing } returns to WHITESPACE mode. Other characters do not change mode.
* SCRIPT mode is slightly different from other modes in that spaces between
* the {/} delimiters and the actual input are not considered significant.
* e.g: " x " is a 3 character string, delimited by double quotes, however
* { x } is a 1 character JavaScript surrounded by whitespace and {}
* delimiters.
* In the short term we assume that the JS routines can make sense of the
* extra whitespace, however at some stage we may need to move the space into
* the Argument prefix/suffix.
* Also we don't attempt to handle nested {}. See bug 678961
*/
SCRIPT: 5
};
/**
* Split up the input taking into account ', " and {.
* We don't consider \t or other classical whitespace characters to split
* arguments apart. For one thing these characters are hard to type, but also
* if the user has gone to the trouble of pasting a TAB character into the
* input field (or whatever it takes), they probably mean it.
*/
Requisition.prototype._tokenize = function(typed) {
// For blank input, place a dummy empty argument into the list
if (typed == null || typed.length === 0) {
return [ new Argument('', '', '') ];
}
if (isSimple(typed)) {
return [ new Argument(typed, '', '') ];
}
var mode = In.WHITESPACE;
// First we un-escape. This list was taken from:
// https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Core_Language_Features#Unicode
// We are generally converting to their real values except for the strings
// '\'', '\"', '\ ', '{' and '}' which we are converting to unicode private
// characters so we can distinguish them from '"', ' ', '{', '}' and ''',
// which are special. They need swapping back post-split - see unescape2()
typed = typed
.replace(/\\\\/g, '\\')
.replace(/\\b/g, '\b')
.replace(/\\f/g, '\f')
.replace(/\\n/g, '\n')
.replace(/\\r/g, '\r')
.replace(/\\t/g, '\t')
.replace(/\\v/g, '\v')
.replace(/\\n/g, '\n')
.replace(/\\r/g, '\r')
.replace(/\\ /g, '\uF000')
.replace(/\\'/g, '\uF001')
.replace(/\\"/g, '\uF002')
.replace(/\\{/g, '\uF003')
.replace(/\\}/g, '\uF004');
function unescape2(escaped) {
return escaped
.replace(/\uF000/g, ' ')
.replace(/\uF001/g, '\'')
.replace(/\uF002/g, '"')
.replace(/\uF003/g, '{')
.replace(/\uF004/g, '}');
}
var i = 0; // The index of the current character
var start = 0; // Where did this section start?
var prefix = ''; // Stuff that comes before the current argument
var args = []; // The array that we're creating
var blockDepth = 0; // For JS with nested {}
// This is just a state machine. We're going through the string char by char
// The 'mode' is one of the 'In' states. As we go, we're adding Arguments
// to the 'args' array.
while (true) {
var c = typed[i];
switch (mode) {
case In.WHITESPACE:
if (c === '\'') {
prefix = typed.substring(start, i + 1);
mode = In.SINGLE_Q;
start = i + 1;
}
else if (c === '"') {
prefix = typed.substring(start, i + 1);
mode = In.DOUBLE_Q;
start = i + 1;
}
else if (c === '{') {
prefix = typed.substring(start, i + 1);
mode = In.SCRIPT;
blockDepth++;
start = i + 1;
}
else if (/ /.test(c)) {
// Still whitespace, do nothing
}
else {
prefix = typed.substring(start, i);
mode = In.SIMPLE;
start = i;
}
break;
case In.SIMPLE:
// There is an edge case of xx'xx which we are assuming to
// be a single parameter (and same with ")
if (c === ' ') {
var str = unescape2(typed.substring(start, i));
args.push(new Argument(str, prefix, ''));
mode = In.WHITESPACE;
start = i;
prefix = '';
}
break;
case In.SINGLE_Q:
if (c === '\'') {
var str = unescape2(typed.substring(start, i));
args.push(new Argument(str, prefix, c));
mode = In.WHITESPACE;
start = i + 1;
prefix = '';
}
break;
case In.DOUBLE_Q:
if (c === '"') {
var str = unescape2(typed.substring(start, i));
args.push(new Argument(str, prefix, c));
mode = In.WHITESPACE;
start = i + 1;
prefix = '';
}
break;
case In.SCRIPT:
if (c === '{') {
blockDepth++;
}
else if (c === '}') {
blockDepth--;
if (blockDepth === 0) {
var str = unescape2(typed.substring(start, i));
args.push(new ScriptArgument(str, prefix, c));
mode = In.WHITESPACE;
start = i + 1;
prefix = '';
}
}
break;
}
i++;
if (i >= typed.length) {
// There is nothing else to read - tidy up
if (mode === In.WHITESPACE) {
if (i !== start) {
// There's whitespace at the end of the typed string. Add it to the
// last argument's suffix, creating an empty argument if needed.
var extra = typed.substring(start, i);
var lastArg = args[args.length - 1];
if (!lastArg) {
args.push(new Argument('', extra, ''));
}
else {
lastArg.suffix += extra;
}
}
}
else if (mode === In.SCRIPT) {
var str = unescape2(typed.substring(start, i + 1));
args.push(new ScriptArgument(str, prefix, ''));
}
else {
var str = unescape2(typed.substring(start, i + 1));
args.push(new Argument(str, prefix, ''));
}
break;
}
}
return args;
};
/**
* If the input has no spaces, quotes, braces or escapes,
* we can take the fast track.
*/
function isSimple(typed) {
for (var i = 0; i < typed.length; i++) {
var c = typed.charAt(i);
if (c === ' ' || c === '"' || c === '\'' ||
c === '{' || c === '}' || c === '\\') {
return false;
}
}
return true;
}
/**
* Looks in the canon for a command extension that matches what has been
* typed at the command line.
*/
Requisition.prototype._split = function(args) {
// Handle the special case of the user typing { javascript(); }
// We use the hidden 'eval' command directly rather than shift()ing one of
// the parameters, and parse()ing it.
if (args[0] instanceof ScriptArgument) {
// Special case: if the user enters { console.log('foo'); } then we need to
// use the hidden 'eval' command
var conversion = new Conversion(evalCommand, new Argument());
this.commandAssignment.setConversion(conversion);
return;
}
var argsUsed = 1;
var conversion;
while (argsUsed <= args.length) {
var arg = (argsUsed === 1) ?
args[0] :
new MergedArgument(args, 0, argsUsed);
conversion = this.commandAssignment.param.type.parse(arg);
// We only want to carry on if this command is a parent command,
// which means that there is a commandAssignment, but not one with
// an exec function.
if (!conversion.value || conversion.value.exec) {
break;
}
// Previously we needed a way to hide commands depending context.
// We have not resurrected that feature yet, but if we do we should
// insert code here to ignore certain commands depending on the
// context/environment
argsUsed++;
}
this.commandAssignment.setConversion(conversion);
for (var i = 0; i < argsUsed; i++) {
args.shift();
}
// This could probably be re-written to consume args as we go
};
/**
* Work out which arguments are applicable to which parameters.
*/
Requisition.prototype._assign = function(args) {
if (!this.commandAssignment.getValue()) {
this._unassigned.setUnassigned(args);
return;
}
if (args.length === 0) {
this.setDefaultArguments();
this._unassigned.setDefault();
return;
}
// Create an error if the command does not take parameters, but we have
// been given them ...
if (this.assignmentCount === 0) {
this._unassigned.setUnassigned(args);
return;
}
// Special case: if there is only 1 parameter, and that's of type
// text, then we put all the params into the first param
if (this.assignmentCount === 1) {
var assignment = this.getAssignment(0);
if (assignment.param.type instanceof StringType) {
var arg = (args.length === 1) ?
args[0] :
new MergedArgument(args);
var conversion = assignment.param.type.parse(arg);
assignment.setConversion(conversion);
this._unassigned.setDefault();
return;
}
}
// Positional arguments can still be specified by name, but if they are
// then we need to ignore them when working them out positionally
var names = this.getParameterNames();
// We collect the arguments used in arrays here before assigning
var arrayArgs = {};
// Extract all the named parameters
this.getAssignments(false).forEach(function(assignment) {
// Loop over the arguments
// Using while rather than loop because we remove args as we go
var i = 0;
while (i < args.length) {
if (assignment.param.isKnownAs(args[i].text)) {
var arg = args.splice(i, 1)[0];
names = names.filter(function(test) {
return test !== assignment.param.name;
});
// boolean parameters don't have values, default to false
if (assignment.param.type instanceof BooleanType) {
arg = new TrueNamedArgument(null, arg);
}
else {
var valueArg = null;
if (i + 1 >= args.length) {
valueArg = args.splice(i, 1)[0];
}
arg = new NamedArgument(arg, valueArg);
}
if (assignment.param.type instanceof ArrayType) {
var arrayArg = arrayArgs[assignment.param.name];
if (!arrayArg) {
arrayArg = new ArrayArgument();
arrayArgs[assignment.param.name] = arrayArg;
}
arrayArg.addArgument(arg);
}
else {
var conversion = assignment.param.type.parse(arg);
assignment.setConversion(conversion);
}
}
else {
// Skip this parameter and handle as a positional parameter
i++;
}
}
}, this);
// What's left are positional parameters assign in order
names.forEach(function(name) {
var assignment = this.getAssignment(name);
// If not set positionally, and we can't set it non-positionally,
// we have to default it to prevent previous values surviving
if (!assignment.param.isPositionalAllowed()) {
assignment.setDefault();
return;
}
// If this is a positional array argument, then it swallows the
// rest of the arguments.
if (assignment.param.type instanceof ArrayType) {
var arrayArg = arrayArgs[assignment.param.name];
if (!arrayArg) {
arrayArg = new ArrayArgument();
arrayArgs[assignment.param.name] = arrayArg;
}
arrayArg.addArguments(args);
args = [];
}
else {
var arg = (args.length > 0) ?
args.splice(0, 1)[0] :
new Argument();
var conversion = assignment.param.type.parse(arg);
assignment.setConversion(conversion);
}
}, this);
// Now we need to assign the array argument (if any)
Object.keys(arrayArgs).forEach(function(name) {
var assignment = this.getAssignment(name);
var conversion = assignment.param.type.parse(arrayArgs[name]);
assignment.setConversion(conversion);
}, this);
if (args.length > 0) {
this._unassigned.setUnassigned(args);
}
else {
this._unassigned.setDefault();
}
};
exports.Requisition = Requisition;
/**
* Functions and data related to the execution of a command
*/
function ExecutionContext(requisition) {
this.requisition = requisition;
this.environment = requisition.environment;
this.document = requisition.document;
}
ExecutionContext.prototype.createPromise = function() {
return new Promise();
};
});
/*
* 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/promise', ['require', 'exports', 'module' ], function(require, exports, module) {
Components.utils.import("resource:///modules/devtools/Promise.jsm");
exports.Promise = Promise;
});
/*
* 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/commands/help', ['require', 'exports', 'module' , 'gcli/canon', 'gcli/util', 'gcli/l10n', 'gcli/ui/domtemplate', 'text!gcli/commands/help.css', 'text!gcli/commands/help_intro.html', 'text!gcli/commands/help_list.html', 'text!gcli/commands/help_man.html'], function(require, exports, module) {
var help = exports;
var canon = require('gcli/canon');
var util = require('gcli/util');
var l10n = require('gcli/l10n');
var domtemplate = require('gcli/ui/domtemplate');
var helpCss = require('text!gcli/commands/help.css');
var helpStyle = undefined;
var helpIntroHtml = require('text!gcli/commands/help_intro.html');
var helpIntroTemplate = undefined;
var helpListHtml = require('text!gcli/commands/help_list.html');
var helpListTemplate = undefined;
var helpManHtml = require('text!gcli/commands/help_man.html');
var helpManTemplate = undefined;
/**
* 'help' command
* We delay definition of helpCommandSpec until help.startup() to ensure that
* the l10n strings have been loaded
*/
var helpCommandSpec;
/**
* Registration and de-registration.
*/
help.startup = function() {
helpCommandSpec = {
name: 'help',
description: l10n.lookup('helpDesc'),
manual: l10n.lookup('helpManual'),
params: [
{
name: 'search',
type: 'string',
description: l10n.lookup('helpSearchDesc'),
manual: l10n.lookup('helpSearchManual'),
defaultValue: null
}
],
returnType: 'html',
exec: function(args, context) {
help.onFirstUseStartup(context.document);
var match = canon.getCommand(args.search);
if (match) {
var clone = helpManTemplate.cloneNode(true);
domtemplate.template(clone, getManTemplateData(match, context),
{ allowEval: true, stack: 'help_man.html' });
return clone;
}
var parent = util.dom.createElement(context.document, 'div');
if (!args.search) {
parent.appendChild(helpIntroTemplate.cloneNode(true));
}
parent.appendChild(helpListTemplate.cloneNode(true));
domtemplate.template(parent, getListTemplateData(args, context),
{ allowEval: true, stack: 'help_intro.html | help_list.html' });
return parent;
}
};
canon.addCommand(helpCommandSpec);
};
help.shutdown = function() {
canon.removeCommand(helpCommandSpec);
helpListTemplate = undefined;
helpStyle.parentElement.removeChild(helpStyle);
helpStyle = undefined;
};
/**
* Called when the command is executed
*/
help.onFirstUseStartup = function(document) {
if (!helpIntroTemplate) {
helpIntroTemplate = util.dom.createElement(document, 'div');
util.dom.setInnerHtml(helpIntroTemplate, helpIntroHtml);
}
if (!helpListTemplate) {
helpListTemplate = util.dom.createElement(document, 'div');
util.dom.setInnerHtml(helpListTemplate, helpListHtml);
}
if (!helpManTemplate) {
helpManTemplate = util.dom.createElement(document, 'div');
util.dom.setInnerHtml(helpManTemplate, helpManHtml);
}
if (!helpStyle && helpCss != null) {
helpStyle = util.dom.importCss(helpCss, document);
}
};
/**
* Find an element within the passed element with the class gcli-help-command
* and update the requisition to contain this text.
*/
function updateCommand(element, context) {
context.requisition.update({
typed: element.querySelector('.gcli-help-command').textContent
});
}
/**
* Find an element within the passed element with the class gcli-help-command
* and execute this text.
*/
function executeCommand(element, context) {
context.requisition.exec({
visible: true,
typed: element.querySelector('.gcli-help-command').textContent
});
}
/**
* Create a block of data suitable to be passed to the help_list.html template
*/
function getListTemplateData(args, context) {
return {
l10n: l10n.propertyLookup,
lang: context.document.defaultView.navigator.language,
onclick: function(ev) {
updateCommand(ev.currentTarget, context);
},
ondblclick: function(ev) {
executeCommand(ev.currentTarget, context);
},
getHeading: function() {
return args.search == null ?
'Available Commands:' :
'Commands starting with \'' + args.search + '\':';
},
getMatchingCommands: function() {
var matching = canon.getCommands().filter(function(command) {
if (args.search && command.name.indexOf(args.search) !== 0) {
// Filtered out because they don't match the search
return false;
}
if (!args.search && command.name.indexOf(' ') != -1) {
// We don't show sub commands with plain 'help'
return false;
}
return true;
});
matching.sort();
return matching;
}
};
}
/**
* Create a block of data suitable to be passed to the help_man.html template
*/
function getManTemplateData(command, context) {
return {
l10n: l10n.propertyLookup,
lang: context.document.defaultView.navigator.language,
command: command,
onclick: function(ev) {
updateCommand(ev.currentTarget, context);
},
getTypeDescription: function(param) {
var input = '';
if (param.defaultValue === undefined) {
input = 'required';
}
else if (param.defaultValue === null) {
input = 'optional';
}
else {
input = param.defaultValue;
}
return '(' + param.type.name + ', ' + input + ')';
}
};
}
});
/*
* 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;
});
define("text!gcli/commands/help.css", [], "");
define("text!gcli/commands/help_intro.html", [], "\n" +
"<h2>${l10n.introHeader}</h2>\n" +
"\n" +
"<p>\n" +
"</p>\n" +
"");
define("text!gcli/commands/help_list.html", [], "\n" +
"<h3>${getHeading()}</h3>\n" +
"\n" +
"<table>\n" +
" <tr foreach=\"command in ${getMatchingCommands()}\"\n" +
" onclick=\"${onclick}\" ondblclick=\"${ondblclick}\">\n" +
" <th class=\"gcli-help-name\">${command.name}</th>\n" +
" <td class=\"gcli-help-arrow\">&#x2192;</td>\n" +
" <td>\n" +
" ${command.description}\n" +
" <span class=\"gcli-out-shortcut gcli-help-command\">help ${command.name}</span>\n" +
" </td>\n" +
" </tr>\n" +
"</table>\n" +
"");
define("text!gcli/commands/help_man.html", [], "\n" +
"<h3>${command.name}</h3>\n" +
"\n" +
"<h4 class=\"gcli-help-header\">\n" +
" ${l10n.helpManSynopsis}:\n" +
" <span class=\"gcli-help-synopsis\" onclick=\"${onclick}\">\n" +
" <span class=\"gcli-help-command\">${command.name}</span>\n" +
" <span foreach=\"param in ${command.params}\">\n" +
" ${param.defaultValue !== undefined ? '[' + param.name + ']' : param.name}\n" +
" </span>\n" +
" </span>\n" +
"</h4>\n" +
"\n" +
"<h4 class=\"gcli-help-header\">${l10n.helpManDescription}:</h4>\n" +
"\n" +
"<p class=\"gcli-help-description\">\n" +
" ${command.manual || command.description}\n" +
"</p>\n" +
"\n" +
"<h4 class=\"gcli-help-header\">${l10n.helpManParameters}:</h4>\n" +
"\n" +
"<ul class=\"gcli-help-parameter\">\n" +
" <li if=\"${command.params.length === 0}\">${l10n.helpManNone}</li>\n" +
" <li foreach=\"param in ${command.params}\">\n" +
" <tt>${param.name}</tt> ${getTypeDescription(param)}\n" +
" <br/>\n" +
" ${param.manual || param.description}\n" +
" </li>\n" +
"</ul>\n" +
"");
/*
* 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/console', ['require', 'exports', 'module' , 'gcli/ui/inputter', 'gcli/ui/arg_fetch', 'gcli/ui/menu', 'gcli/ui/focus'], function(require, exports, module) {
var Inputter = require('gcli/ui/inputter').Inputter;
var ArgFetcher = require('gcli/ui/arg_fetch').ArgFetcher;
var CommandMenu = require('gcli/ui/menu').CommandMenu;
var FocusManager = require('gcli/ui/focus').FocusManager;
/**
* Console is responsible for generating the UI for GCLI, this implementation
* is a special case for use inside Firefox
*/
function Console(options) {
this.hintElement = options.hintElement;
this.gcliTerm = options.gcliTerm;
this.consoleWrap = options.consoleWrap;
this.requisition = options.requisition;
// Create a FocusManager for the various parts to register with
this.focusManager = new FocusManager({ document: options.chromeDocument });
this.focusManager.onFocus.add(this.gcliTerm.show, this.gcliTerm);
this.focusManager.onBlur.add(this.gcliTerm.hide, this.gcliTerm);
this.focusManager.addMonitoredElement(this.gcliTerm.hintNode, 'gcliTerm');
this.inputter = new Inputter({
document: options.chromeDocument,
requisition: options.requisition,
inputElement: options.inputElement,
completeElement: options.completeElement,
completionPrompt: '',
backgroundElement: options.backgroundElement,
focusManager: this.focusManager,
scratchpad: options.scratchpad
});
this.menu = new CommandMenu({
document: options.chromeDocument,
requisition: options.requisition,
menuClass: 'gcliterm-menu'
});
this.hintElement.appendChild(this.menu.element);
this.argFetcher = new ArgFetcher({
document: options.chromeDocument,
requisition: options.requisition,
argFetcherClass: 'gcliterm-argfetcher'
});
this.hintElement.appendChild(this.argFetcher.element);
this.chromeWindow = options.chromeDocument.defaultView;
this.resizer = this.resizer.bind(this);
this.chromeWindow.addEventListener('resize', this.resizer, false);
this.requisition.commandChange.add(this.resizer, this);
}
/**
* Avoid memory leaks
*/
Console.prototype.destroy = function() {
this.chromeWindow.removeEventListener('resize', this.resizer, false);
delete this.resizer;
delete this.chromeWindow;
delete this.consoleWrap;
this.hintElement.removeChild(this.menu.element);
this.menu.destroy();
this.hintElement.removeChild(this.argFetcher.element);
this.argFetcher.destroy();
this.inputter.destroy();
this.focusManager.removeMonitoredElement(this.gcliTerm.hintNode, 'gcliTerm');
this.focusManager.onFocus.remove(this.gcliTerm.show, this.gcliTerm);
this.focusManager.onBlur.remove(this.gcliTerm.hide, this.gcliTerm);
this.focusManager.destroy();
delete this.gcliTerm;
delete this.hintElement;
};
/**
* Called on chrome window resize, or on divider slide
*/
Console.prototype.resizer = function() {
// Bug 705109: There are several numbers hard-coded in this function.
// This is simpler than calculating them, but error-prone when the UI setup,
// the styling or display settings change.
var parentRect = this.consoleWrap.getBoundingClientRect();
// Magic number: 64 is the size of the toolbar above the output area
var parentHeight = parentRect.bottom - parentRect.top - 64;
// Magic number: 100 is the size at which we decide the hints are too small
// to be useful, so we hide them
if (parentHeight < 100) {
this.hintElement.classList.add('gcliterm-hint-nospace');
}
else {
this.hintElement.classList.remove('gcliterm-hint-nospace');
var isMenuVisible = this.menu.element.style.display !== 'none';
if (isMenuVisible) {
this.menu.setMaxHeight(parentHeight);
// Magic numbers: 19 = height of a menu item, 22 = total vertical padding
// of container
var idealMenuHeight = (19 * this.menu.items.length) + 22;
if (idealMenuHeight > parentHeight) {
this.hintElement.classList.add('gcliterm-hint-scroll');
}
else {
this.hintElement.classList.remove('gcliterm-hint-scroll');
}
}
else {
this.argFetcher.setMaxHeight(parentHeight);
this.hintElement.style.overflowY = null;
this.hintElement.style.borderBottomColor = 'white';
}
}
// We also try to make the max-width of any GCLI elements so they don't
// extend outside the scroll area.
var doc = this.hintElement.ownerDocument;
var outputNode = this.hintElement.parentNode.parentNode.children[1];
var outputs = outputNode.getElementsByClassName('gcliterm-msg-body');
var listItems = outputNode.getElementsByClassName('hud-msg-node');
// This is an top-side estimate. We could try to calculate it, maybe using
// something along these lines http://www.alexandre-gomes.com/?p=115 However
// experience has shown this to be hard to get to work reliably
// Also we don't need to be precise. If we use a number that is too big then
// the only down-side is too great a right margin
var scrollbarWidth = 20;
if (listItems.length > 0) {
var parentWidth = outputNode.getBoundingClientRect().width - scrollbarWidth;
var otherWidth;
var body;
for (var i = 0; i < listItems.length; ++i) {
var listItem = listItems[i];
// a.k.a 'var otherWidth = 132'
otherWidth = 0;
body = null;
for (var j = 0; j < listItem.children.length; j++) {
var child = listItem.children[j];
if (child.classList.contains('gcliterm-msg-body')) {
body = child.children[0];
}
else {
otherWidth += child.getBoundingClientRect().width;
}
var styles = doc.defaultView.getComputedStyle(child, null);
otherWidth += parseInt(styles.borderLeftWidth, 10) +
parseInt(styles.borderRightWidth, 10) +
parseInt(styles.paddingLeft, 10) +
parseInt(styles.paddingRight, 10) +
parseInt(styles.marginLeft, 10) +
parseInt(styles.marginRight, 10);
}
if (body) {
body.style.width = (parentWidth - otherWidth) + 'px';
}
}
}
};
exports.Console = Console;
});
/*
* 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/inputter', ['require', 'exports', 'module' , 'gcli/util', 'gcli/l10n', 'gcli/types', 'gcli/history', 'text!gcli/ui/inputter.css'], function(require, exports, module) {
var cliView = exports;
var KeyEvent = require('gcli/util').event.KeyEvent;
var dom = require('gcli/util').dom;
var l10n = require('gcli/l10n');
var Status = require('gcli/types').Status;
var History = require('gcli/history').History;
var inputterCss = require('text!gcli/ui/inputter.css');
/**
* A wrapper to take care of the functions concerning an input element
*/
function Inputter(options) {
this.requisition = options.requisition;
this.scratchpad = options.scratchpad;
// Suss out where the input element is
this.element = options.inputElement || 'gcli-input';
if (typeof this.element === 'string') {
this.document = options.document || document;
var name = this.element;
this.element = this.document.getElementById(name);
if (!this.element) {
throw new Error('No element with id=' + name + '.');
}
}
else {
// Assume we've been passed in the correct node
this.document = this.element.ownerDocument;
}
if (inputterCss != null) {
this.style = dom.importCss(inputterCss, this.document);
}
this.element.spellcheck = false;
// Used to distinguish focus from TAB in CLI. See onKeyUp()
this.lastTabDownAt = 0;
// Used to effect caret changes. See _processCaretChange()
this._caretChange = null;
// Ensure that TAB/UP/DOWN isn't handled by the browser
this.onKeyDown = this.onKeyDown.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
this.element.addEventListener('keydown', this.onKeyDown, false);
this.element.addEventListener('keyup', this.onKeyUp, false);
this.completer = options.completer || new Completer(options);
this.completer.decorate(this);
// Use the provided history object, or instantiate our own
this.history = options.history || new History(options);
this._scrollingThroughHistory = false;
// Cursor position affects hint severity
this.onMouseUp = function(ev) {
this.completer.update(this.getInputState());
}.bind(this);
this.element.addEventListener('mouseup', this.onMouseUp, false);
this.focusManager = options.focusManager;
if (this.focusManager) {
this.focusManager.addMonitoredElement(this.element, 'input');
}
this.requisition.inputChange.add(this.onInputChange, this);
this.update();
}
/**
* Avoid memory leaks
*/
Inputter.prototype.destroy = function() {
this.requisition.inputChange.remove(this.onInputChange, this);
if (this.focusManager) {
this.focusManager.removeMonitoredElement(this.element, 'input');
}
this.element.removeEventListener('keydown', this.onKeyDown, false);
this.element.removeEventListener('keyup', this.onKeyUp, false);
delete this.onKeyDown;
delete this.onKeyUp;
this.history.destroy();
this.completer.destroy();
if (this.style) {
this.style.parentNode.removeChild(this.style);
delete this.style;
}
delete this.document;
delete this.element;
};
/**
* Utility to add an element into the DOM after the input element
*/
Inputter.prototype.appendAfter = function(element) {
this.element.parentNode.insertBefore(element, this.element.nextSibling);
};
/**
* Handler for the Requisition.inputChange event
*/
Inputter.prototype.onInputChange = function() {
if (this._caretChange == null) {
// We weren't expecting a change so this was requested by the hint system
// we should move the cursor to the end of the 'changed section', and the
// best we can do for that right now is the end of the current argument.
this._caretChange = Caret.TO_ARG_END;
}
this._setInputInternal(this.requisition.toString());
};
/**
* Internal function to set the input field to a value.
* This function checks to see if the current value is the same as the new
* value, and makes no changes if they are the same (except for caret/completer
* updating - see below). If changes are to be made, they are done in a timeout
* to avoid XUL bug 676520.
* This function assumes that the data model is up to date with the new value.
* It does attempts to leave the caret position in the same position in the
* input string unless this._caretChange === Caret.TO_ARG_END. This is required
* for completion events.
* It does not change the completer decoration unless this._updatePending is
* set. This is required for completion events.
*/
Inputter.prototype._setInputInternal = function(str, update) {
if (!this.document) {
return; // This can happen post-destroy()
}
if (this.element.value && this.element.value === str) {
this._processCaretChange(this.getInputState(), false);
return;
}
// Updating in a timeout fixes a XUL issue (bug 676520) where textbox gives
// incorrect values for its content
this.document.defaultView.setTimeout(function() {
if (!this.document) {
return; // This can happen post-destroy()
}
// Bug 678520 - We could do better caret handling by recording the caret
// position in terms of offset into an assignment, and then replacing into
// a similar place
var input = this.getInputState();
input.typed = str;
this._processCaretChange(input);
this.element.value = str;
if (update) {
this.update();
}
}.bind(this), 0);
};
/**
* Various ways in which we need to manipulate the caret/selection position.
* A value of null means we're not expecting a change
*/
var Caret = {
/**
* We are expecting changes, but we don't need to move the cursor
*/
NO_CHANGE: 0,
/**
* We want the entire input area to be selected
*/
SELECT_ALL: 1,
/**
* The whole input has changed - push the cursor to the end
*/
TO_END: 2,
/**
* A part of the input has changed - push the cursor to the end of the
* changed section
*/
TO_ARG_END: 3
};
/**
* If this._caretChange === Caret.TO_ARG_END, we alter the input object to move
* the selection start to the end of the current argument.
* @param input An object shaped like { typed:'', cursor: { start:0, end:0 }}
* @param forceUpdate Do we call this.completer.update even when the cursor has
* not changed (useful when input.typed has changed)
*/
Inputter.prototype._processCaretChange = function(input, forceUpdate) {
var start, end;
switch (this._caretChange) {
case Caret.SELECT_ALL:
start = 0;
end = input.typed.length;
break;
case Caret.TO_END:
start = input.typed.length;
end = input.typed.length;
break;
case Caret.TO_ARG_END:
// There could be a fancy way to do this involving assignment/arg math
// but it doesn't seem easy, so we cheat a move the cursor to just before
// the next space, or the end of the input
start = input.cursor.start;
do {
start++;
}
while (start < input.typed.length && input.typed[start - 1] !== ' ');
end = start;
break;
case null:
case Caret.NO_CHANGE:
start = input.cursor.start;
end = input.cursor.end;
break;
}
start = (start > input.typed.length) ? input.typed.length : start;
end = (end > input.typed.length) ? input.typed.length : end;
var newInput = { typed: input.typed, cursor: { start: start, end: end }};
if (start !== input.cursor.start || end !== input.cursor.end || forceUpdate) {
this.completer.update(newInput);
}
this.element.selectionStart = newInput.cursor.start;
this.element.selectionEnd = newInput.cursor.end;
this._caretChange = null;
return newInput;
};
/**
* Set the input field to a value.
* This function updates the data model and the completer decoration. It sets
* the caret to the end of the input. It does not make any similarity checks
* so calling this function with it's current value resets the cursor position.
* It does not execute the input or affect the history.
* This function should not be called internally, by Inputter and never as a
* result of a keyboard event on this.element or bug 676520 could be triggered.
*/
Inputter.prototype.setInput = function(str) {
this.element.value = str;
this.update();
};
/**
* Focus the input element
*/
Inputter.prototype.focus = function() {
this.element.focus();
};
/**
* Ensure certain keys (arrows, tab, etc) that we would like to handle
* are not handled by the browser
*/
Inputter.prototype.onKeyDown = function(ev) {
if (ev.keyCode === KeyEvent.DOM_VK_UP || ev.keyCode === KeyEvent.DOM_VK_DOWN) {
ev.preventDefault();
}
if (ev.keyCode === KeyEvent.DOM_VK_TAB) {
this.lastTabDownAt = 0;
if (!ev.shiftKey) {
ev.preventDefault();
// Record the timestamp of this TAB down so onKeyUp can distinguish
// focus from TAB in the CLI.
this.lastTabDownAt = ev.timeStamp;
}
if (ev.metaKey || ev.altKey || ev.crtlKey) {
if (this.document.commandDispatcher) {
this.document.commandDispatcher.advanceFocus();
}
else {
this.element.blur();
}
}
}
};
/**
* The main keyboard processing loop
*/
Inputter.prototype.onKeyUp = function(ev) {
// Give the scratchpad (if enabled) a chance to activate
if (this.scratchpad && this.scratchpad.shouldActivate(ev)) {
if (this.scratchpad.activate(this.element.value)) {
this._setInputInternal('', true);
}
return;
}
// RETURN does a special exec/highlight thing
if (ev.keyCode === KeyEvent.DOM_VK_RETURN) {
var worst = this.requisition.getStatus();
// Deny RETURN unless the command might work
if (worst === Status.VALID) {
this._scrollingThroughHistory = false;
this.history.add(this.element.value);
this.requisition.exec();
}
// See bug 664135 - On pressing return with an invalid input, GCLI
// should select the incorrect part of the input for an easy fix
return;
}
if (ev.keyCode === KeyEvent.DOM_VK_TAB && !ev.shiftKey) {
// If the TAB keypress took the cursor from another field to this one,
// then they get the keydown/keypress, and we get the keyup. In this
// case we don't want to do any completion.
// If the time of the keydown/keypress of TAB was close (i.e. within
// 1 second) to the time of the keyup then we assume that we got them
// both, and do the completion.
if (this.lastTabDownAt + 1000 > ev.timeStamp) {
// It's possible for TAB to not change the input, in which case the
// onInputChange event will not fire, and the caret move will not be
// processed. So we check that this is done first
this._caretChange = Caret.TO_ARG_END;
this._processCaretChange(this.getInputState(), true);
this.getCurrentAssignment().complete();
}
this.lastTabDownAt = 0;
this._scrollingThroughHistory = false;
return;
}
if (ev.keyCode === KeyEvent.DOM_VK_UP) {
if (this.element.value === '' || this._scrollingThroughHistory) {
this._scrollingThroughHistory = true;
this._setInputInternal(this.history.backward(), true);
}
else {
this.getCurrentAssignment().increment();
}
return;
}
if (ev.keyCode === KeyEvent.DOM_VK_DOWN) {
if (this.element.value === '' || this._scrollingThroughHistory) {
this._scrollingThroughHistory = true;
this._setInputInternal(this.history.forward(), true);
}
else {
this.getCurrentAssignment().decrement();
}
return;
}
this._scrollingThroughHistory = false;
this._caretChange = Caret.NO_CHANGE;
this.update();
};
/**
* Accessor for the assignment at the cursor.
* i.e Requisition.getAssignmentAt(cursorPos);
*/
Inputter.prototype.getCurrentAssignment = function() {
var start = this.element.selectionStart;
return this.requisition.getAssignmentAt(start);
};
/**
* Actually parse the input and make sure we're all up to date
*/
Inputter.prototype.update = function() {
var input = this.getInputState();
this.requisition.update(input);
this.completer.update(input);
};
/**
* Pull together an input object, which may include XUL hacks
*/
Inputter.prototype.getInputState = function() {
var input = {
typed: this.element.value,
cursor: {
start: this.element.selectionStart,
end: this.element.selectionEnd
}
};
// Workaround for potential XUL bug 676520 where textbox gives incorrect
// values for its content
if (input.typed == null) {
input = { typed: '', cursor: { start: 0, end: 0 } };
console.log('fixing input.typed=""', input);
}
// Workaround for a Bug 717268 (which is really a jsdom bug)
if (input.cursor.start == null) {
input.cursor.start = 0;
}
return input;
};
cliView.Inputter = Inputter;
/**
* Completer is an 'input-like' element that sits an input element annotating
* it with visual goodness.
* @param {object} options An object that contains various options which
* customizes how the completer functions.
* Properties on the options object:
* - document (required) DOM document to be used in creating elements
* - requisition (required) A GCLI Requisition object whose state is monitored
* - completeElement (optional) An element to use
* - completionPrompt (optional) The prompt - defaults to '\u00bb'
* (double greater-than, a.k.a right guillemet). The prompt is used directly
* in a TextNode, so HTML entities are not allowed.
*/
function Completer(options) {
this.document = options.document || document;
this.requisition = options.requisition;
this.elementCreated = false;
this.scratchpad = options.scratchpad;
this.element = options.completeElement || 'gcli-row-complete';
if (typeof this.element === 'string') {
var name = this.element;
this.element = this.document.getElementById(name);
if (!this.element) {
this.elementCreated = true;
this.element = dom.createElement(this.document, 'div');
this.element.className = 'gcli-in-complete gcli-in-valid';
this.element.setAttribute('tabindex', '-1');
this.element.setAttribute('aria-live', 'polite');
}
}
this.completionPrompt = typeof options.completionPrompt === 'string'
? options.completionPrompt
: '\u00bb';
if (options.inputBackgroundElement) {
this.backgroundElement = options.inputBackgroundElement;
}
else {
this.backgroundElement = this.element;
}
}
/**
* Avoid memory leaks
*/
Completer.prototype.destroy = function() {
delete this.document;
delete this.element;
delete this.backgroundElement;
if (this.elementCreated) {
this.document.defaultView.removeEventListener('resize', this.resizer, false);
}
delete this.inputter;
};
/**
* A list of the styles that decorate() should copy to make the completion
* element look like the input element. backgroundColor is a spiritual part of
* this list, but see comment in decorate().
*/
Completer.copyStyles = [ 'fontSize', 'fontFamily', 'fontWeight', 'fontStyle' ];
/**
* Make ourselves visually similar to the input element, and make the input
* element transparent so our background shines through
*/
Completer.prototype.decorate = function(inputter) {
this.inputter = inputter;
var input = inputter.element;
// If we were told which element to use, then assume it is already
// correctly positioned. Otherwise insert it alongside the input element
if (this.elementCreated) {
this.inputter.appendAfter(this.element);
var styles = this.document.defaultView.getComputedStyle(input, null);
Completer.copyStyles.forEach(function(style) {
this.element.style[style] = styles[style];
}, this);
// The completer text is by default invisible so we make it the same color
// as the input background.
this.element.style.color = input.style.backgroundColor;
// If there is a separate backgroundElement, then we make the element
// transparent, otherwise it inherits the color of the input node
// It's not clear why backgroundColor doesn't work when used from
// computedStyle, but it doesn't. Patches welcome!
this.element.style.backgroundColor = (this.backgroundElement != this.element) ?
'transparent' :
input.style.backgroundColor;
input.style.backgroundColor = 'transparent';
// Make room for the prompt
input.style.paddingLeft = '20px';
this.resizer = this.resizer.bind(this);
this.document.defaultView.addEventListener('resize', this.resizer, false);
this.resizer();
}
};
/**
* Ensure that the completion element is the same size and the inputter element
*/
Completer.prototype.resizer = function() {
// Remove this when jsdom does getBoundingClientRect(). See Bug 717269
if (!this.inputter.element.getBoundingClientRect) {
return;
}
var rect = this.inputter.element.getBoundingClientRect();
// -4 to line up with 1px of padding and border, top and bottom
var height = rect.bottom - rect.top - 4;
this.element.style.top = rect.top + 'px';
this.element.style.height = height + 'px';
this.element.style.lineHeight = height + 'px';
this.element.style.left = rect.left + 'px';
this.element.style.width = (rect.right - rect.left) + 'px';
};
/**
* Is the completion given, a "strict" completion of the user inputted value?
* A completion is considered "strict" only if it the user inputted value is an
* exact prefix of the completion (ignoring leading whitespace)
*/
function isStrictCompletion(inputValue, completion) {
// Strip any leading whitespace from the user inputted value because the
// completion will never have leading whitespace.
inputValue = inputValue.replace(/^\s*/, '');
// Strict: "ec" -> "echo"
// Non-Strict: "ls *" -> "ls foo bar baz"
return completion.indexOf(inputValue) === 0;
}
/**
* Bring the completion element up to date with what the requisition says
*/
Completer.prototype.update = function(input) {
var current = this.requisition.getAssignmentAt(input.cursor.start);
var predictions = current.getPredictions();
dom.clearElement(this.element);
// All this DOM manipulation is equivalent to the HTML below.
// It's not a template because it's very simple except appendMarkupStatus()
// which is complex due to a need to merge spans.
// Bug 707131 questions if we couldn't simplify this to use a template.
//
// <span class="gcli-prompt">${completionPrompt}</span>
// ${appendMarkupStatus()}
// ${prefix}
// <span class="gcli-in-ontab">${contents}</span>
// <span class="gcli-in-closebrace" if="${unclosedJs}">}<span>
// <div class="gcli-in-scratchlink">${scratchLink}</div>
var document = this.element.ownerDocument;
var prompt = dom.createElement(document, 'span');
prompt.classList.add('gcli-prompt');
prompt.appendChild(document.createTextNode(this.completionPrompt + ' '));
this.element.appendChild(prompt);
if (input.typed.length > 0) {
var scores = this.requisition.getInputStatusMarkup(input.cursor.start);
this.appendMarkupStatus(this.element, scores, input);
}
if (input.typed.length > 0 && predictions.length > 0) {
var tab = predictions[0].name;
var existing = current.getArg().text;
var contents;
var prefix = null;
if (isStrictCompletion(existing, tab) &&
input.cursor.start === input.typed.length) {
// Display the suffix of the prediction as the completion
var numLeadingSpaces = existing.match(/^(\s*)/)[0].length;
contents = tab.slice(existing.length - numLeadingSpaces);
} else {
// Display the '-> prediction' at the end of the completer element
prefix = ' \u00a0'; // aka &nbsp;
contents = '\u21E5 ' + tab; // aka &rarr; the right arrow
}
if (prefix != null) {
this.element.appendChild(document.createTextNode(prefix));
}
var suffix = dom.createElement(document, 'span');
suffix.classList.add('gcli-in-ontab');
suffix.appendChild(document.createTextNode(contents));
this.element.appendChild(suffix);
}
// Add a grey '}' to the end of the command line when we've opened
// with a { but haven't closed it
var command = this.requisition.commandAssignment.getValue();
var isJsCommand = (command && command.name === '{');
var isUnclosedJs = isJsCommand &&
this.requisition.getAssignment(0).getArg().suffix.indexOf('}') === -1;
if (isUnclosedJs) {
var close = dom.createElement(document, 'span');
close.classList.add('gcli-in-closebrace');
close.appendChild(document.createTextNode(' }'));
this.element.appendChild(close);
}
// Create a scratchpad link if it's a JS command and we have a function to
// actually perform the request
if (isJsCommand && this.scratchpad) {
var hint = dom.createElement(document, 'div');
hint.classList.add('gcli-in-scratchlink');
hint.appendChild(document.createTextNode(this.scratchpad.linkText));
this.element.appendChild(hint);
}
};
/**
* Mark-up an array of Status values with spans
*/
Completer.prototype.appendMarkupStatus = function(element, scores, input) {
if (scores.length === 0) {
return;
}
var document = element.ownerDocument;
var i = 0;
var lastStatus = -1;
var span;
var contents = '';
while (true) {
if (lastStatus !== scores[i]) {
var state = scores[i];
if (!state) {
console.error('No state at i=' + i + '. scores.len=' + scores.length);
state = Status.VALID;
}
span = dom.createElement(document, 'span');
span.classList.add('gcli-in-' + state.toString().toLowerCase());
lastStatus = scores[i];
}
var char = input.typed[i];
if (char === ' ') {
char = '\u00a0';
}
contents += char;
i++;
if (i === input.typed.length) {
span.appendChild(document.createTextNode(contents));
this.element.appendChild(span);
break;
}
if (lastStatus !== scores[i]) {
span.appendChild(document.createTextNode(contents));
this.element.appendChild(span);
contents = '';
}
}
};
cliView.Completer = Completer;
});
/*
* 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/history', ['require', 'exports', 'module' ], function(require, exports, module) {
/**
* A History object remembers commands that have been entered in the past and
* provides an API for accessing them again.
* See Bug 681340: Search through history (like C-r in bash)?
*/
function History() {
// This is the actual buffer where previous commands are kept.
// 'this._buffer[0]' should always be equal the empty string. This is so
// that when you try to go in to the "future", you will just get an empty
// command.
this._buffer = [''];
// This is an index in to the history buffer which points to where we
// currently are in the history.
this._current = 0;
}
/**
* Avoid memory leaks
*/
History.prototype.destroy = function() {
// delete this._buffer;
};
/**
* Record and save a new command in the history.
*/
History.prototype.add = function(command) {
this._buffer.splice(1, 0, command);
this._current = 0;
};
/**
* Get the next (newer) command from history.
*/
History.prototype.forward = function() {
if (this._current > 0 ) {
this._current--;
}
return this._buffer[this._current];
};
/**
* Get the previous (older) item from history.
*/
History.prototype.backward = function() {
if (this._current < this._buffer.length - 1) {
this._current++;
}
return this._buffer[this._current];
};
exports.History = History;
});define("text!gcli/ui/inputter.css", [], "");
/*
* 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/arg_fetch', ['require', 'exports', 'module' , 'gcli/util', 'gcli/types', 'gcli/ui/field', 'gcli/ui/domtemplate', 'text!gcli/ui/arg_fetch.css', 'text!gcli/ui/arg_fetch.html'], function(require, exports, module) {
var argFetch = exports;
var dom = require('gcli/util').dom;
var Status = require('gcli/types').Status;
var getField = require('gcli/ui/field').getField;
var domtemplate = require('gcli/ui/domtemplate');
var editorCss = require('text!gcli/ui/arg_fetch.css');
var argFetchHtml = require('text!gcli/ui/arg_fetch.html');
/**
* A widget to display an inline dialog which allows the user to fill out
* the arguments to a command.
* @param options An object containing the customizations, which include:
* - document: The document to use in creating widgets
* - requisition: The Requisition to fill out
* - argFetcherClass: Custom class name when generating the top level element
* which allows different layout systems
*/
function ArgFetcher(options) {
this.document = options.document || document;
this.requisition = options.requisition;
// FF can be really hard to debug if doc is null, so we check early on
if (!this.document) {
throw new Error('No document');
}
this.element = dom.createElement(this.document, 'div');
this.element.className = options.argFetcherClass || 'gcli-argfetch';
// We cache the fields we create so we can destroy them later
this.fields = [];
// Populated by template
this.okElement = null;
// Pull the HTML into the DOM, but don't add it to the document
if (editorCss != null) {
this.style = dom.importCss(editorCss, this.document);
}
var templates = dom.createElement(this.document, 'div');
dom.setInnerHtml(templates, argFetchHtml);
this.reqTempl = templates.querySelector('.gcli-af-template');
this.requisition.commandChange.add(this.onCommandChange, this);
this.requisition.inputChange.add(this.onInputChange, this);
this.onCommandChange();
}
/**
* Avoid memory leaks
*/
ArgFetcher.prototype.destroy = function() {
this.requisition.inputChange.remove(this.onInputChange, this);
this.requisition.commandChange.remove(this.onCommandChange, this);
if (this.style) {
this.style.parentNode.removeChild(this.style);
delete this.style;
}
this.fields.forEach(function(field) { field.destroy(); });
delete this.document;
delete this.element;
delete this.okElement;
delete this.reqTempl;
};
/**
* Called whenever the command part of the requisition changes
*/
ArgFetcher.prototype.onCommandChange = function(ev) {
var command = this.requisition.commandAssignment.getValue();
if (!command || !command.exec) {
this.element.style.display = 'none';
}
else {
if (ev && ev.oldValue === ev.newValue) {
// Just the text has changed
return;
}
this.fields.forEach(function(field) { field.destroy(); });
this.fields = [];
var reqEle = this.reqTempl.cloneNode(true);
domtemplate.template(reqEle, this,
{ allowEval: true, stack: 'arg_fetch.html' });
dom.clearElement(this.element);
this.element.appendChild(reqEle);
var status = this.requisition.getStatus();
this.okElement.disabled = (status === Status.VALID);
this.element.style.display = 'block';
}
};
/**
* Called whenever the text input of the requisition changes
*/
ArgFetcher.prototype.onInputChange = function(ev) {
var command = this.requisition.commandAssignment.getValue();
if (command && command.exec) {
var status = this.requisition.getStatus();
this.okElement.disabled = (status !== Status.VALID);
}
};
/**
* Called by the template process in #onCommandChange() to get an instance
* of field for each assignment.
*/
ArgFetcher.prototype.getInputFor = function(assignment) {
try {
var newField = getField(assignment.param.type, {
document: this.document,
type: assignment.param.type,
name: assignment.param.name,
requisition: this.requisition,
required: assignment.param.isDataRequired(),
named: !assignment.param.isPositionalAllowed()
});
// BUG 664198 - remove on delete
newField.fieldChanged.add(function(ev) {
assignment.setConversion(ev.conversion);
}, this);
assignment.assignmentChange.add(function(ev) {
newField.setConversion(ev.conversion);
}.bind(this));
this.fields.push(newField);
newField.setConversion(this.assignment.conversion);
// Bug 681894: we add the field as a property of the assignment so that
// #linkMessageElement() can call 'field.setMessageElement(element)'
assignment.field = newField;
return newField.element;
}
catch (ex) {
// This is called from within template() which can make tracing errors hard
// so we log here if anything goes wrong
console.error(ex);
return '';
}
};
/**
* Called by the template to setup an mutable message field
*/
ArgFetcher.prototype.linkMessageElement = function(assignment, element) {
// Bug 681894: See comment in getInputFor()
var field = assignment.field;
delete assignment.field;
if (field == null) {
console.error('Missing field for ' + assignment.param.name);
return 'Missing field';
}
field.setMessageElement(element);
return '';
};
/**
* Event handler added by the template menu.html
*/
ArgFetcher.prototype.onFormOk = function(ev) {
this.requisition.exec();
};
/**
* Event handler added by the template menu.html
*/
ArgFetcher.prototype.onFormCancel = function(ev) {
this.requisition.clear();
};
/**
* Change how much vertical space this dialog can take up
*/
ArgFetcher.prototype.setMaxHeight = function(height, isTooBig) {
this.fields.forEach(function(field) {
if (field.menu) {
// Magic number alert: 105 is roughly the size taken up by the rest of
// the dialog for the '{' command. We could spend ages calculating 105
// by doing math on the various components that contribute to the 105,
// but I don't think that would make it significantly less fragile under
// refactoring. Plus this works.
field.menu.setMaxHeight(height - 105);
}
});
};
argFetch.ArgFetcher = ArgFetcher;
});
/*
* 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/field', ['require', 'exports', 'module' , 'gcli/util', 'gcli/l10n', 'gcli/argument', 'gcli/types', 'gcli/types/basic', 'gcli/types/javascript', 'gcli/ui/menu'], function(require, exports, module) {
var dom = require('gcli/util').dom;
var createEvent = require('gcli/util').createEvent;
var l10n = require('gcli/l10n');
var Argument = require('gcli/argument').Argument;
var TrueNamedArgument = require('gcli/argument').TrueNamedArgument;
var FalseNamedArgument = require('gcli/argument').FalseNamedArgument;
var ArrayArgument = require('gcli/argument').ArrayArgument;
var Status = require('gcli/types').Status;
var Conversion = require('gcli/types').Conversion;
var ArrayConversion = require('gcli/types').ArrayConversion;
var StringType = require('gcli/types/basic').StringType;
var NumberType = require('gcli/types/basic').NumberType;
var BooleanType = require('gcli/types/basic').BooleanType;
var BlankType = require('gcli/types/basic').BlankType;
var SelectionType = require('gcli/types/basic').SelectionType;
var DeferredType = require('gcli/types/basic').DeferredType;
var ArrayType = require('gcli/types/basic').ArrayType;
var JavascriptType = require('gcli/types/javascript').JavascriptType;
var Menu = require('gcli/ui/menu').Menu;
/**
* A Field is a way to get input for a single parameter.
* This class is designed to be inherited from. It's important that all
* subclasses have a similar constructor signature because they are created
* via getField(...)
* @param document The document we use in calling createElement
* @param type The type to use in conversions
* @param named Is this parameter named? That is to say, are positional
* arguments disallowed, if true, then we need to provide updates to the
* command line that explicitly name the parameter in use (e.g. --verbose, or
* --name Fred rather than just true or Fred)
* @param name If this parameter is named, what name should we use
* @param requ The requisition that we're attached to
*/
function Field(document, type, named, name, requ) {
}
/**
* Subclasses should assign their element with the DOM node that gets added
* to the 'form'. It doesn't have to be an input node, just something that
* contains it.
*/
Field.prototype.element = undefined;
/**
* Indicates that this field should drop any resources that it has created
*/
Field.prototype.destroy = function() {
delete this.messageElement;
};
/**
* Update this field display with the value from this conversion.
* Subclasses should provide an implementation of this function.
*/
Field.prototype.setConversion = function(conversion) {
throw new Error('Field should not be used directly');
};
/**
* Extract a conversion from the values in this field.
* Subclasses should provide an implementation of this function.
*/
Field.prototype.getConversion = function() {
throw new Error('Field should not be used directly');
};
/**
* Set the element where messages and validation errors will be displayed
* @see setMessage()
*/
Field.prototype.setMessageElement = function(element) {
this.messageElement = element;
};
/**
* Display a validation message in the UI
*/
Field.prototype.setMessage = function(message) {
if (this.messageElement) {
if (message == null) {
message = '';
}
dom.setInnerHtml(this.messageElement, message);
}
};
/**
* Method to be called by subclasses when their input changes, which allows us
* to properly pass on the fieldChanged event.
*/
Field.prototype.onInputChange = function() {
var conversion = this.getConversion();
this.fieldChanged({ conversion: conversion });
this.setMessage(conversion.message);
};
/**
* 'static/abstract' method to allow implementations of Field to lay a claim
* to a type. This allows claims of various strength to be weighted up.
* See the Field.*MATCH values.
*/
Field.claim = function() {
throw new Error('Field should not be used directly');
};
Field.MATCH = 5;
Field.DEFAULT_MATCH = 4;
Field.IF_NOTHING_BETTER = 1;
Field.NO_MATCH = 0;
/**
* Managing the current list of Fields
*/
var fieldCtors = [];
function addField(fieldCtor) {
if (typeof fieldCtor !== 'function') {
console.error('addField erroring on ', fieldCtor);
throw new Error('addField requires a Field constructor');
}
fieldCtors.push(fieldCtor);
}
function removeField(field) {
if (typeof field !== 'string') {
fields = fields.filter(function(test) {
return test !== field;
});
delete fields[field];
}
else if (field instanceof Field) {
removeField(field.name);
}
else {
console.error('removeField erroring on ', field);
throw new Error('removeField requires an instance of Field');
}
}
function getField(type, options) {
var ctor;
var highestClaim = -1;
fieldCtors.forEach(function(fieldCtor) {
var claim = fieldCtor.claim(type);
if (claim > highestClaim) {
highestClaim = claim;
ctor = fieldCtor;
}
});
if (!ctor) {
console.error('Unknown field type ', type, ' in ', fieldCtors);
throw new Error('Can\'t find field for ' + type);
}
return new ctor(type, options);
}
exports.Field = Field;
exports.addField = addField;
exports.removeField = removeField;
exports.getField = getField;
/**
* A field that allows editing of strings
*/
function StringField(type, options) {
this.document = options.document;
this.type = type;
this.arg = new Argument();
this.element = dom.createElement(this.document, 'input');
this.element.type = 'text';
this.element.classList.add('gcli-field');
this.onInputChange = this.onInputChange.bind(this);
this.element.addEventListener('keyup', this.onInputChange, false);
this.fieldChanged = createEvent('StringField.fieldChanged');
}
StringField.prototype = Object.create(Field.prototype);
StringField.prototype.destroy = function() {
Field.prototype.destroy.call(this);
this.element.removeEventListener('keyup', this.onInputChange, false);
delete this.element;
delete this.document;
delete this.onInputChange;
};
StringField.prototype.setConversion = function(conversion) {
this.arg = conversion.arg;
this.element.value = conversion.arg.text;
this.setMessage(conversion.message);
};
StringField.prototype.getConversion = function() {
// This tweaks the prefix/suffix of the argument to fit
this.arg = this.arg.beget(this.element.value, { prefixSpace: true });
return this.type.parse(this.arg);
};
StringField.claim = function(type) {
return type instanceof StringType ? Field.MATCH : Field.IF_NOTHING_BETTER;
};
exports.StringField = StringField;
addField(StringField);
/**
* A field that allows editing of numbers using an [input type=number] field
*/
function NumberField(type, options) {
this.document = options.document;
this.type = type;
this.arg = new Argument();
this.element = dom.createElement(this.document, 'input');
this.element.type = 'number';
if (this.type.max) {
this.element.max = this.type.max;
}
if (this.type.min) {
this.element.min = this.type.min;
}
if (this.type.step) {
this.element.step = this.type.step;
}
this.onInputChange = this.onInputChange.bind(this);
this.element.addEventListener('keyup', this.onInputChange, false);
this.fieldChanged = createEvent('NumberField.fieldChanged');
}
NumberField.prototype = Object.create(Field.prototype);
NumberField.claim = function(type) {
return type instanceof NumberType ? Field.MATCH : Field.NO_MATCH;
};
NumberField.prototype.destroy = function() {
Field.prototype.destroy.call(this);
this.element.removeEventListener('keyup', this.onInputChange, false);
delete this.element;
delete this.document;
delete this.onInputChange;
};
NumberField.prototype.setConversion = function(conversion) {
this.arg = conversion.arg;
this.element.value = conversion.arg.text;
this.setMessage(conversion.message);
};
NumberField.prototype.getConversion = function() {
this.arg = this.arg.beget(this.element.value, { prefixSpace: true });
return this.type.parse(this.arg);
};
exports.NumberField = NumberField;
addField(NumberField);
/**
* A field that uses a checkbox to toggle a boolean field
*/
function BooleanField(type, options) {
this.document = options.document;
this.type = type;
this.name = options.name;
this.named = options.named;
this.element = dom.createElement(this.document, 'input');
this.element.type = 'checkbox';
this.element.id = 'gcliForm' + this.name;
this.onInputChange = this.onInputChange.bind(this);
this.element.addEventListener('change', this.onInputChange, false);
this.fieldChanged = createEvent('BooleanField.fieldChanged');
}
BooleanField.prototype = Object.create(Field.prototype);
BooleanField.claim = function(type) {
return type instanceof BooleanType ? Field.MATCH : Field.NO_MATCH;
};
BooleanField.prototype.destroy = function() {
Field.prototype.destroy.call(this);
this.element.removeEventListener('change', this.onInputChange, false);
delete this.element;
delete this.document;
delete this.onInputChange;
};
BooleanField.prototype.setConversion = function(conversion) {
this.element.checked = conversion.value;
this.setMessage(conversion.message);
};
BooleanField.prototype.getConversion = function() {
var value = this.element.checked;
var arg = this.named ?
value ? new TrueNamedArgument(this.name) : new FalseNamedArgument() :
new Argument(' ' + value);
return new Conversion(value, arg);
};
exports.BooleanField = BooleanField;
addField(BooleanField);
/**
* Model an instanceof SelectionType as a select input box.
* <p>There are 3 slightly overlapping concepts to be aware of:
* <ul>
* <li>value: This is the (probably non-string) value, known as a value by the
* assignment
* <li>optValue: This is the text value as known by the DOM option element, as
* in &lt;option value=???%gt...
* <li>optText: This is the contents of the DOM option element.
* </ul>
*/
function SelectionField(type, options) {
this.document = options.document;
this.type = type;
this.items = [];
this.element = dom.createElement(this.document, 'select');
this.element.classList.add('gcli-field');
this._addOption({
name: l10n.lookupFormat('fieldSelectionSelect', [ options.name ])
});
var lookup = this.type.getLookup();
lookup.forEach(this._addOption, this);
this.onInputChange = this.onInputChange.bind(this);
this.element.addEventListener('change', this.onInputChange, false);
this.fieldChanged = createEvent('SelectionField.fieldChanged');
}
SelectionField.prototype = Object.create(Field.prototype);
SelectionField.claim = function(type) {
return type instanceof SelectionType ? Field.DEFAULT_MATCH : Field.NO_MATCH;
};
SelectionField.prototype.destroy = function() {
Field.prototype.destroy.call(this);
this.element.removeEventListener('change', this.onInputChange, false);
delete this.element;
delete this.document;
delete this.onInputChange;
};
SelectionField.prototype.setConversion = function(conversion) {
var index;
this.items.forEach(function(item) {
if (item.value && item.value === conversion.value) {
index = item.index;
}
}, this);
this.element.value = index;
this.setMessage(conversion.message);
};
SelectionField.prototype.getConversion = function() {
var item = this.items[this.element.value];
var arg = new Argument(item.name, ' ');
var value = item.value ? item.value : item;
return new Conversion(value, arg);
};
SelectionField.prototype._addOption = function(item) {
item.index = this.items.length;
this.items.push(item);
var option = dom.createElement(this.document, 'option');
option.innerHTML = item.name;
option.value = item.index;
this.element.appendChild(option);
};
exports.SelectionField = SelectionField;
addField(SelectionField);
/**
* A field that allows editing of javascript
*/
function JavascriptField(type, options) {
this.document = options.document;
this.type = type;
this.requ = options.requisition;
this.onInputChange = this.onInputChange.bind(this);
this.arg = new Argument('', '{ ', ' }');
this.element = dom.createElement(this.document, 'div');
this.input = dom.createElement(this.document, 'input');
this.input.type = 'text';
this.input.addEventListener('keyup', this.onInputChange, false);
this.input.classList.add('gcli-field');
this.input.classList.add('gcli-field-javascript');
this.element.appendChild(this.input);
this.menu = new Menu({ document: this.document, field: true });
this.element.appendChild(this.menu.element);
this.setConversion(this.type.parse(new Argument('')));
this.fieldChanged = createEvent('JavascriptField.fieldChanged');
// i.e. Register this.onItemClick as the default action for a menu click
this.menu.onItemClick = this.onItemClick.bind(this);
}
JavascriptField.prototype = Object.create(Field.prototype);
JavascriptField.claim = function(type) {
return type instanceof JavascriptType ? Field.MATCH : Field.NO_MATCH;
};
JavascriptField.prototype.destroy = function() {
Field.prototype.destroy.call(this);
this.input.removeEventListener('keyup', this.onInputChange, false);
this.menu.destroy();
delete this.element;
delete this.input;
delete this.menu;
delete this.document;
delete this.onInputChange;
};
JavascriptField.prototype.setConversion = function(conversion) {
this.arg = conversion.arg;
this.input.value = conversion.arg.text;
var prefixLen = 0;
if (this.type instanceof JavascriptType) {
var typed = conversion.arg.text;
var lastDot = typed.lastIndexOf('.');
if (lastDot !== -1) {
prefixLen = lastDot;
}
}
var items = [];
var predictions = conversion.getPredictions();
predictions.forEach(function(item) {
// Commands can be hidden
if (!item.hidden) {
items.push({
name: item.name.substring(prefixLen),
complete: item.name,
description: item.description || ''
});
}
}, this);
this.menu.show(items);
this.setMessage(conversion.message);
};
JavascriptField.prototype.onItemClick = function(ev) {
this.item = ev.currentTarget.item;
this.arg = this.arg.beget(this.item.complete, { normalize: true });
var conversion = this.type.parse(this.arg);
this.fieldChanged({ conversion: conversion });
this.setMessage(conversion.message);
};
JavascriptField.prototype.onInputChange = function(ev) {
this.item = ev.currentTarget.item;
var conversion = this.getConversion();
this.fieldChanged({ conversion: conversion });
this.setMessage(conversion.message);
};
JavascriptField.prototype.getConversion = function() {
// This tweaks the prefix/suffix of the argument to fit
this.arg = this.arg.beget(this.input.value, { normalize: true });
return this.type.parse(this.arg);
};
JavascriptField.DEFAULT_VALUE = '__JavascriptField.DEFAULT_VALUE';
exports.JavascriptField = JavascriptField;
addField(JavascriptField);
/**
* A field that works with deferred types by delaying resolution until that
* last possible time
*/
function DeferredField(type, options) {
this.document = options.document;
this.type = type;
this.options = options;
this.requisition = options.requisition;
this.requisition.assignmentChange.add(this.update, this);
this.element = dom.createElement(this.document, 'div');
this.update();
this.fieldChanged = createEvent('DeferredField.fieldChanged');
}
DeferredField.prototype = Object.create(Field.prototype);
DeferredField.prototype.update = function() {
var subtype = this.type.defer();
if (subtype === this.subtype) {
return;
}
if (this.field) {
this.field.destroy();
}
this.subtype = subtype;
this.field = getField(subtype, this.options);
this.field.fieldChanged.add(this.fieldChanged, this);
dom.clearElement(this.element);
this.element.appendChild(this.field.element);
};
DeferredField.claim = function(type) {
return type instanceof DeferredType ? Field.MATCH : Field.NO_MATCH;
};
DeferredField.prototype.destroy = function() {
Field.prototype.destroy.call(this);
this.requisition.assignmentChange.remove(this.update, this);
delete this.element;
delete this.document;
delete this.onInputChange;
};
DeferredField.prototype.setConversion = function(conversion) {
this.field.setConversion(conversion);
};
DeferredField.prototype.getConversion = function() {
return this.field.getConversion();
};
exports.DeferredField = DeferredField;
addField(DeferredField);
/**
* For use with deferred types that do not yet have anything to resolve to.
* BlankFields are not for general use.
*/
function BlankField(type, options) {
this.document = options.document;
this.type = type;
this.element = dom.createElement(this.document, 'div');
this.fieldChanged = createEvent('BlankField.fieldChanged');
}
BlankField.prototype = Object.create(Field.prototype);
BlankField.claim = function(type) {
return type instanceof BlankType ? Field.MATCH : Field.NO_MATCH;
};
BlankField.prototype.setConversion = function() { };
BlankField.prototype.getConversion = function() {
return new Conversion(null);
};
exports.BlankField = BlankField;
addField(BlankField);
/**
* Adds add/delete buttons to a normal field allowing there to be many values
* given for a parameter.
*/
function ArrayField(type, options) {
this.document = options.document;
this.type = type;
this.options = options;
this.requ = options.requisition;
this._onAdd = this._onAdd.bind(this);
this.members = [];
// <div class=gcliArrayParent save="${element}">
this.element = dom.createElement(this.document, 'div');
this.element.classList.add('gcli-array-parent');
// <button class=gcliArrayMbrAdd onclick="${_onAdd}" save="${addButton}">Add
this.addButton = dom.createElement(this.document, 'button');
this.addButton.classList.add('gcli-array-member-add');
this.addButton.addEventListener('click', this._onAdd, false);
this.addButton.innerHTML = l10n.lookup('fieldArrayAdd');
this.element.appendChild(this.addButton);
// <div class=gcliArrayMbrs save="${mbrElement}">
this.container = dom.createElement(this.document, 'div');
this.container.classList.add('gcli-array-members');
this.element.appendChild(this.container);
this.onInputChange = this.onInputChange.bind(this);
this.fieldChanged = createEvent('ArrayField.fieldChanged');
}
ArrayField.prototype = Object.create(Field.prototype);
ArrayField.claim = function(type) {
return type instanceof ArrayType ? Field.MATCH : Field.NO_MATCH;
};
ArrayField.prototype.destroy = function() {
Field.prototype.destroy.call(this);
this.addButton.removeEventListener('click', this._onAdd, false);
};
ArrayField.prototype.setConversion = function(conversion) {
// BUG 653568: this is too brutal - it removes focus from any the current field
dom.clearElement(this.container);
this.members = [];
conversion.conversions.forEach(function(subConversion) {
this._onAdd(null, subConversion);
}, this);
};
ArrayField.prototype.getConversion = function() {
var conversions = [];
var arrayArg = new ArrayArgument();
for (var i = 0; i < this.members.length; i++) {
var conversion = this.members[i].field.getConversion();
conversions.push(conversion);
arrayArg.addArgument(conversion.arg);
}
return new ArrayConversion(conversions, arrayArg);
};
ArrayField.prototype._onAdd = function(ev, subConversion) {
// <div class=gcliArrayMbr save="${element}">
var element = dom.createElement(this.document, 'div');
element.classList.add('gcli-array-member');
this.container.appendChild(element);
// ${field.element}
var field = getField(this.type.subtype, this.options);
field.fieldChanged.add(function() {
var conversion = this.getConversion();
this.fieldChanged({ conversion: conversion });
this.setMessage(conversion.message);
}, this);
if (subConversion) {
field.setConversion(subConversion);
}
element.appendChild(field.element);
// <div class=gcliArrayMbrDel onclick="${_onDel}">
var delButton = dom.createElement(this.document, 'button');
delButton.classList.add('gcli-array-member-del');
delButton.addEventListener('click', this._onDel, false);
delButton.innerHTML = l10n.lookup('fieldArrayDel');
element.appendChild(delButton);
var member = {
element: element,
field: field,
parent: this
};
member.onDelete = function() {
this.parent.container.removeChild(this.element);
this.parent.members = this.parent.members.filter(function(test) {
return test !== this;
});
this.parent.onInputChange();
}.bind(member);
delButton.addEventListener('click', member.onDelete, false);
this.members.push(member);
};
exports.ArrayField = ArrayField;
addField(ArrayField);
});
/*
* 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/menu', ['require', 'exports', 'module' , 'gcli/util', 'gcli/types', 'gcli/argument', 'gcli/canon', 'gcli/ui/domtemplate', 'text!gcli/ui/menu.css', 'text!gcli/ui/menu.html'], function(require, exports, module) {
var dom = require('gcli/util').dom;
var Conversion = require('gcli/types').Conversion;
var Argument = require('gcli/argument').Argument;
var canon = require('gcli/canon');
var domtemplate = require('gcli/ui/domtemplate');
var menuCss = require('text!gcli/ui/menu.css');
var menuHtml = require('text!gcli/ui/menu.html');
/**
* Menu is a display of the commands that are possible given the state of a
* requisition.
* @param options A way to customize the menu display. Valid options are:
* - field: [boolean] Turns the menu display into a drop-down for use inside a
* JavascriptField.
* - document: The document to use in creating widgets
* - menuClass: Custom class name when generating the top level element
* which allows different layout systems
*/
function Menu(options) {
options = options || {};
this.document = options.document || document;
// FF can be really hard to debug if doc is null, so we check early on
if (!this.document) {
throw new Error('No document');
}
this.element = dom.createElement(this.document, 'div');
this.element.classList.add(options.menuClass || 'gcli-menu');
if (options && options.field) {
this.element.classList.add(options.menuFieldClass || 'gcli-menu-field');
}
// Pull the HTML into the DOM, but don't add it to the document
if (menuCss != null) {
this.style = dom.importCss(menuCss, this.document);
}
var templates = dom.createElement(this.document, 'div');
dom.setInnerHtml(templates, menuHtml);
this.optTempl = templates.querySelector('.gcli-menu-template');
// Contains the items that should be displayed
this.items = null;
}
/**
* Avoid memory leaks
*/
Menu.prototype.destroy = function() {
if (this.style) {
this.style.parentNode.removeChild(this.style);
delete this.style;
}
delete this.element;
delete this.items;
delete this.optTempl;
};
/**
* The default is to do nothing when someone clicks on the menu.
* Plug an implementation in here before calling show() to do something useful.
* This is called from template.html
* @param ev The click event from the browser
*/
Menu.prototype.onItemClick = function(ev) {
};
/**
* Display a number of items in the menu (or hide the menu if there is nothing
* to display)
* @param items The items to show in the menu
* @param error An error message to display
*/
Menu.prototype.show = function(items, error) {
this.error = error;
this.items = items;
if (this.error == null && this.items.length === 0) {
this.element.style.display = 'none';
return;
}
var options = this.optTempl.cloneNode(true);
domtemplate.template(options, this, { allowEval: true, stack: 'menu.html' });
dom.clearElement(this.element);
this.element.appendChild(options);
this.element.style.display = 'block';
};
/**
* Hide the menu
*/
Menu.prototype.hide = function() {
this.element.style.display = 'none';
};
/**
* Change how much vertical space this menu can take up
*/
Menu.prototype.setMaxHeight = function(height) {
this.element.style.maxHeight = height + 'px';
};
exports.Menu = Menu;
/**
* CommandMenu is a special menu that integrates with a Requisition to display
* available commands.
* @param options A way to customize the menu display. Valid options include
* those valid for Menu(), plus:
* - requisition: The Requisition to fill out (required)
*/
function CommandMenu(options) {
Menu.call(this, options);
this.requisition = options.requisition;
this.requisition.commandChange.add(this.onCommandChange, this);
canon.canonChange.add(this.onCommandChange, this);
this.onCommandChange();
}
CommandMenu.prototype = Object.create(Menu.prototype);
/**
* Avoid memory leaks
*/
CommandMenu.prototype.destroy = function() {
this.requisition.commandChange.remove(this.onCommandChange, this);
canon.canonChange.remove(this.onCommandChange, this);
Menu.prototype.destroy.call(this);
};
/**
* We want to fill-in the clicked command in the cli input when the user clicks
*/
CommandMenu.prototype.onItemClick = function(ev) {
var type = this.requisition.commandAssignment.param.type;
var name = ev.currentTarget.querySelector('.gcli-menu-name').innerHTML;
var arg = new Argument(name);
arg.suffix = ' ';
var conversion = type.parse(arg);
this.requisition.commandAssignment.setConversion(conversion);
};
/**
* Update the various hint components to reflect the changed command
*/
CommandMenu.prototype.onCommandChange = function(ev) {
var command = this.requisition.commandAssignment.getValue();
if (!command || !command.exec) {
var error = this.requisition.commandAssignment.getMessage();
var predictions = this.requisition.commandAssignment.getPredictions();
if (predictions.length === 0) {
var commandType = this.requisition.commandAssignment.param.type;
var conversion = commandType.parse(new Argument());
predictions = conversion.getPredictions();
}
predictions.sort(function(command1, command2) {
return command1.name.localeCompare(command2.name);
});
var items = [];
predictions.forEach(function(item) {
if (item.description && !item.hidden) {
items.push(item);
}
}, this);
this.show(items, error);
}
else {
if (ev && ev.oldValue === ev.newValue) {
return; // Just the text has changed
}
this.hide();
}
};
exports.CommandMenu = CommandMenu;
});
define("text!gcli/ui/menu.css", [], "");
define("text!gcli/ui/menu.html", [], "\n" +
"<table class=\"gcli-menu-template\" aria-live=\"polite\">\n" +
" <tr class=\"gcli-menu-option\" foreach=\"item in ${items}\"\n" +
" onclick=\"${onItemClick}\" title=\"${item.manual || ''}\">\n" +
" <td class=\"gcli-menu-name\">${item.name}</td>\n" +
" <td class=\"gcli-menu-desc\">${item.description}</td>\n" +
" </tr>\n" +
" <tr if=\"${error}\">\n" +
" <td class=\"gcli-menu-error\" colspan=\"2\">${error}</td>\n" +
" </tr>\n" +
"</table>\n" +
"");
define("text!gcli/ui/arg_fetch.css", [], "");
define("text!gcli/ui/arg_fetch.html", [], "\n" +
"<!--\n" +
"Template for an Assignment.\n" +
"Evaluated each time the commandAssignment changes\n" +
"-->\n" +
"<div class=\"gcli-af-template\" aria-live=\"polite\">\n" +
" <div>\n" +
" <div class=\"gcli-af-cmddesc\">\n" +
" ${requisition.commandAssignment.getValue().description}\n" +
" </div>\n" +
" <table class=\"gcli-af-params\">\n" +
" <tbody foreach=\"assignment in ${requisition.getAssignments()}\">\n" +
" <!-- Parameter -->\n" +
" <tr>\n" +
" <td class=\"gcli-af-paramname\">\n" +
" <label for=\"gcliForm${assignment.param.name}\">\n" +
" ${assignment.param.description ? assignment.param.description + ':' : ''}\n" +
" </label>\n" +
" </td>\n" +
" <td>${getInputFor(assignment)}</td>\n" +
" <td>\n" +
" <span class=\"gcli-af-required\" if=\"${assignment.param.isDataRequired()}\">*</span>\n" +
" </td>\n" +
" </tr>\n" +
" <tr>\n" +
" <td class=\"gcli-af-error\" colspan=\"2\">\n" +
" ${linkMessageElement(assignment, __element)}\n" +
" </td>\n" +
" </tr>\n" +
" </tbody>\n" +
" <tfoot>\n" +
" <tr>\n" +
" <td colspan=\"3\" class=\"gcli-af-submit\">\n" +
" <input type=\"submit\" value=\"Cancel\" onclick=\"${onFormCancel}\"/>\n" +
" <input type=\"submit\" value=\"OK\" onclick=\"${onFormOk}\" save=\"${okElement}\"/>\n" +
" </td>\n" +
" </tr>\n" +
" </tfoot>\n" +
" </table>\n" +
" </div>\n" +
"</div>\n" +
"");
/*
* 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/focus', ['require', 'exports', 'module' , 'gcli/util'], function(require, exports, module) {
var util = require('gcli/util');
/**
* FocusManager solves the problem of tracking focus among a set of nodes.
* The specific problem we are solving is when the hint element must be visible
* if either the command line or any of the inputs in the hint element has the
* focus, and invisible at other times, without hiding and showing the hint
* element even briefly as the focus changes between them.
* It does this simply by postponing the hide events by 250ms to see if
* something else takes focus.
* @param options An optional object containing configuration values. Valid
* properties on the options object are:
* - document
* - blurDelay
* - debug
* - initialFocus
*/
function FocusManager(options) {
options = options || {};
this._debug = options.debug || false;
this._blurDelayTimeout = null; // Result of setTimeout in delaying a blur
this._monitoredElements = []; // See addMonitoredElement()
this.hasFocus = false;
this.blurDelay = options.blurDelay || 250;
this.document = options.document || document;
this.onFocus = util.createEvent('FocusManager.onFocus');
this.onBlur = util.createEvent('FocusManager.onBlur');
// We take a focus event anywhere to be an indication that we might be about
// to lose focus
this._onDocumentFocus = function() {
this.reportBlur('document');
}.bind(this);
this.document.addEventListener('focus', this._onDocumentFocus, true);
}
/**
* Avoid memory leaks
*/
FocusManager.prototype.destroy = function() {
this.document.removeEventListener('focus', this._onDocumentFocus, true);
delete this.document;
for (var i = 0; i < this._monitoredElements.length; i++) {
var monitor = this._monitoredElements[i];
console.error('Hanging monitored element: ', monitor.element);
monitor.element.removeEventListener('focus', monitor.onFocus, true);
monitor.element.removeEventListener('blur', monitor.onBlur, true);
}
if (this._blurDelayTimeout) {
clearTimeout(this._blurDelayTimeout);
this._blurDelayTimeout = null;
}
};
/**
* The easy way to include an element in the set of things that are part of the
* aggregate focus. Using [add|remove]MonitoredElement() is a simpler way of
* option than calling report[Focus|Blur]()
* @param element The element on which to track focus|blur events
* @param where Optional source string for debugging only
*/
FocusManager.prototype.addMonitoredElement = function(element, where) {
if (this._debug) {
console.log('FocusManager.addMonitoredElement(' + (where || 'unknown') + ')');
}
var monitor = {
element: element,
where: where,
onFocus: function() { this.reportFocus(where); }.bind(this),
onBlur: function() { this.reportBlur(where); }.bind(this)
};
element.addEventListener('focus', monitor.onFocus, true);
element.addEventListener('blur', monitor.onBlur, true);
this._monitoredElements.push(monitor);
};
/**
* Undo the effects of addMonitoredElement()
* @param element The element to stop tracking
*/
FocusManager.prototype.removeMonitoredElement = function(element) {
var monitor;
var matchIndex;
for (var i = 0; i < this._monitoredElements.length; i++) {
if (this._monitoredElements[i].element === element) {
monitor = this._monitoredElements[i];
matchIndex = i;
}
}
if (!monitor) {
if (this._debug) {
console.error('Missing monitor for element. ', element);
}
return;
}
this._monitoredElements.splice(matchIndex, 1);
element.removeEventListener('focus', monitor.onFocus, true);
element.removeEventListener('blur', monitor.onBlur, true);
};
/**
* Some component has received a 'focus' event. This sets the internal status
* straight away and informs the listeners
* @param where Optional source string for debugging only
*/
FocusManager.prototype.reportFocus = function(where) {
if (this._debug) {
console.log('FocusManager.reportFocus(' + (where || 'unknown') + ')');
}
if (this._blurDelayTimeout) {
if (this._debug) {
console.log('FocusManager.cancelBlur');
}
clearTimeout(this._blurDelayTimeout);
this._blurDelayTimeout = null;
}
if (!this.hasFocus) {
this.hasFocus = true;
this.onFocus();
}
};
/**
* Some component has received a 'blur' event. This waits for a while to see if
* we are going to get any subsequent 'focus' events and then sets the internal
* status and informs the listeners
* @param where Optional source string for debugging only
*/
FocusManager.prototype.reportBlur = function(where) {
if (this._debug) {
console.log('FocusManager.reportBlur(' + where + ')');
}
if (this.hasFocus) {
if (this._blurDelayTimeout) {
if (this._debug) {
console.log('FocusManager.blurPending');
}
return;
}
this._blurDelayTimeout = setTimeout(function() {
if (this._debug) {
console.log('FocusManager.blur');
}
this.hasFocus = false;
this.onBlur();
this._blurDelayTimeout = null;
}.bind(this), this.blurDelay);
}
};
exports.FocusManager = FocusManager;
});
/*
* require GCLI so it can be exported as declared in EXPORTED_SYMBOLS
* The dependencies specified here should be the same as in Makefile.dryice.js
*/
var gcli = require("gcli/index");