From 27a355c977694afac169526de452da2e5daa0fee Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Fri, 27 May 2011 12:35:18 +0100 Subject: [PATCH] Bug 656668 - Create Export from GCLI project to JSM; r=rcampbell, msucan, fitzgen, mratcliffe --- browser/devtools/webconsole/gcli.jsm | 7610 ++++++++++++++++++++++++-- 1 file changed, 7165 insertions(+), 445 deletions(-) diff --git a/browser/devtools/webconsole/gcli.jsm b/browser/devtools/webconsole/gcli.jsm index 5935f3a435d..396d5baffd5 100644 --- a/browser/devtools/webconsole/gcli.jsm +++ b/browser/devtools/webconsole/gcli.jsm @@ -1,39 +1,8 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is GCLI. - * - * The Initial Developer of the Original Code is - * The Mozilla Foundation - * Portions created by the Initial Developer are Copyright (C) 2011 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Joe Walker (Original Author) - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ +/* + * Copyright 2009-2011 Mozilla Foundation and contributors + * Licensed under the New BSD license. See LICENSE.txt or: + * http://opensource.org/licenses/BSD-3-Clause + */ /* * @@ -48,9 +17,28 @@ * 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 + * ******************************************************************************* * * @@ -65,337 +53,429 @@ /////////////////////////////////////////////////////////////////////////////// -/* - * This build of GCLI for Firefox is really 4 bits of code: - * - Browser support code - Currently just an implementation of the console - * object that uses dump. We may need to add other browser shims to this. - * - 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 section of code is a copy of mini_require.js without the header and - * footers. Changes to one should be reflected in the other. - * - A build of GCLI itself, packaged using dryice (for more details see the - * project https://github.com/mozilla/dryice and the build file in this - * project at Makefile.dryice.js) - * - Lastly, code to require the gcli object as needed by EXPORTED_SYMBOLS. - */ - 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) */ - - -/** - * 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 == undefined) { - aMinLen = aMaxLen; - } - if (aStr == null) { - aStr = ""; - } - if (aStr.length > aMaxLen) { - if (aOptions && aOptions.truncate == "start") { - return "_" + aStr.substring(aStr.length - aMaxLen + 1); +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; } - else { - return aStr.substring(0, aMaxLen - 1) + "_"; + if (aStr == null) { + aStr = ""; } - } - 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 60 chars long - */ -function stringify(aThing) { - if (aThing === undefined) { - return "undefined"; - } - - if (aThing === null) { - return "null"; - } - - if (typeof aThing == "object") { - try { - return getCtorName(aThing) + " " + fmt(JSON.stringify(aThing), 50, 0); - } - catch (ex) { - return "[stringify error]"; - } - } - - var str = aThing.toString().replace(/\s+/g, " "); - return fmt(str, 60, 0); -} - -/** - * 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"; - } - - if (aThing == undefined) { - return "undefined"; - } - - if (typeof aThing == "object") { - var reply = ""; - var type = getCtorName(aThing); - if (type == "Error") { - reply += " " + aThing.message + "\n"; - reply += logProperty("stack", aThing.stack); - } - else { - var keys = Object.getOwnPropertyNames(aThing); - if (keys.length > 0) { - reply += type + "\n"; - keys.forEach(function(aProp) { - reply += logProperty(aProp, aThing[aProp]); - }, this); + if (aStr.length > aMaxLen) { + if (aOptions && aOptions.truncate == "start") { + return "_" + aStr.substring(aStr.length - aMaxLen + 1); } else { - reply += type + " (enumerated with for-in)\n"; - var prop; - for (prop in aThing) { - reply += logProperty(prop, aThing[prop]); - } + 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); + } + + var str = aThing.toString(); //.replace(/\s+/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 + " (enumerated with for-in)\n"; + var prop; + for (prop in aThing) { + reply += logProperty(prop, aThing[prop]); + } + } + } + + 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; } - 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); + /** + * 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; } - 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; + /** + * 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 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; } - var trace = []; - while (aFrame) { - trace.push({ - file: aFrame.filename, - line: aFrame.lineNumber, - call: aFrame.name + + /** + * 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"; }); - aFrame = aFrame.caller; + return reply; } - 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 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)); + }); + }; + } -/** - * 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)); - }); - }; -} - -/** - * The console object to expose - */ -var console = { - debug: createMultiLineDumper("debug"), - log: createDumper("log"), - info: createDumper("info"), - warn: createDumper("warn"), - error: createMultiLineDumper("error"), - trace: function Console_trace() { + /** + * 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"); }, - clear: function Console_clear() {}, + console.clear = function Console_clear() {}; - dir: createMultiLineDumper("dir"), - dirxml: createMultiLineDumper("dirxml"), - group: createDumper("group"), - groupEnd: createDumper("groupEnd") -}; + 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 + */ -/////////////////////////////////////////////////////////////////////////////// - -// There are 2 virtually identical copies of this code: -// - $GCLI_HOME/build/prefix-gcli.jsm -// - $GCLI_HOME/build/mini_require.js -// They should both be kept in sync - -var debugDependencies = false; /** * Define a module along with a payload. - * @param {string} moduleName Name for the payload - * @param {ignored} deps Ignored. For compatibility with CommonJS AMD Spec - * @param {function} payload Function with (require, exports, module) params + * @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") { @@ -408,7 +488,7 @@ function define(moduleName, deps, payload) { payload = deps; } - if (debugDependencies) { + if (define.debugDependencies) { console.log("define: " + moduleName + " -> " + payload.toString() .slice(0, 40).replace(/\n/, '\\n').replace(/\r/, '\\r') + "..."); } @@ -417,151 +497,6791 @@ function define(moduleName, deps, payload) { 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; + /** - * 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. + * Self executing function in which Domain is defined, and attached to define */ -function Domain() { - this.modules = {}; +(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 (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 {string[]|string} deps A name, or names for the payload - * @param {function|undefined} callback Function to call when the dependencies - * are resolved - * @return {undefined|object} 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); + if (define.debugDependencies) { + this.depth = ""; } - return undefined; } - else { - return this.lookup(deps); - } -}; -/** - * Lookup module names and resolve them by calling the definition function if - * needed. - * @param {string} moduleName A name for the payload to lookup - * @return {object} 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 (debugDependencies) { - console.log(this.depth + " Using module: " + moduleName); + /** + * 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; - } + }; - if (!(moduleName in define.modules)) { - console.error(this.depth + " Missing module: " + moduleName); - return null; - } - - var module = define.modules[moduleName]; - - if (debugDependencies) { - console.log(this.depth + " Compiling module: " + moduleName); - } - - if (typeof module == "function") { - if (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 (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 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); - - -/////////////////////////////////////////////////////////////////////////////// - /* - * The API of interest to people wanting to create GCLI commands is as - * follows. The implementation of this API is left to bug 659061 and other - * bugs. + * 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/index', [ ], function(require, exports, module) { +define('gcli/index', ['require', 'exports', 'module' , 'gcli/canon', 'gcli/types/basic', 'gcli/types/javascript', 'gcli/types/node', 'gcli/cli', 'gcli/ui/inputter', 'gcli/ui/arg_fetch', 'gcli/ui/menu', 'gcli/ui/focus'], function(require, exports, module) { + + // The API for use by command authors + exports.addCommand = require('gcli/canon').addCommand; + exports.removeCommand = require('gcli/canon').removeCommand; + + // Internal startup process. Not exported + require('gcli/types/basic').startup(); + require('gcli/types/javascript').startup(); + require('gcli/types/node').startup(); + require('gcli/cli').startup(); + + var Requisition = require('gcli/cli').Requisition; + var cli = require('gcli/cli'); + 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; + + 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) { + opts.autoHide = true; + opts.requisition = new Requisition(opts.environment, opts.chromeDocument); + opts.completionPrompt = ''; + + jstype.setGlobalObject(opts.jsEnvironment.globalObject); + nodetype.setDocument(opts.contentDocument); + cli.setEvalFunction(opts.jsEnvironment.evalFunction); + + // Create a FocusManager for the various parts to register with + if (!opts.focusManager) { + opts.debug = true; + opts.focusManager = new FocusManager({ document: opts.chromeDocument }); + } + + opts.inputter = new Inputter(opts); + opts.inputter.update(); + if (opts.gcliTerm) { + opts.focusManager.onFocus.add(opts.gcliTerm.show, opts.gcliTerm); + opts.focusManager.onBlur.add(opts.gcliTerm.hide, opts.gcliTerm); + opts.focusManager.addMonitoredElement(opts.gcliTerm.hintNode, 'gcliTerm'); + } + + if (opts.hintElement) { + opts.menu = new CommandMenu(opts.chromeDocument, opts.requisition); + opts.hintElement.appendChild(opts.menu.element); + + opts.argFetcher = new ArgFetcher(opts.chromeDocument, opts.requisition); + opts.hintElement.appendChild(opts.argFetcher.element); + + opts.menu.onCommandChange(); + } + }, + + /** + * Undo the effects of createView() to prevent memory leaks + */ + removeView: function(opts) { + opts.hintElement.removeChild(opts.menu.element); + opts.menu.destroy(); + opts.hintElement.removeChild(opts.argFetcher.element); + opts.argFetcher.destroy(); + + opts.inputter.destroy(); + opts.focusManager.removeMonitoredElement(opts.gcliTerm.hintNode, 'gcliTerm'); + opts.focusManager.onFocus.remove(opts.gcliTerm.show, opts.gcliTerm); + opts.focusManager.onBlur.remove(opts.gcliTerm.hide, opts.gcliTerm); + opts.focusManager.destroy(); + + cli.unsetEvalFunction(); + nodetype.unsetDocument(); + jstype.unsetGlobalObject(); + + opts.requisition.destroy(); + }, + + 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 createEvent = require('gcli/util').createEvent; +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) { + return commands[name]; +}; + +/** + * 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 = 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 = 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(); - exports.addCommand = function() { /* implementation goes here */ }; - exports.removeCommand = function() { /* implementation goes here */ }; - exports.startup = function() { /* implementation goes here */ }; - exports.shutdown = function() { /* implementation goes here */ }; }); +/* + * 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) { + +/* + * This module is a Pilot-Lite. It exports a number of objects that replicate + * parts of the Pilot project. It aims to be mostly API compatible, while + * removing the submodule complexity and helping us make things work inside + * Firefox. + * The Pilot compatible exports are: console/dom/event + * + * In addition it contains a small event library similar to EventEmitter but + * which makes it harder to mistake the event in use. + */ + + +//------------------------------------------------------------------------------ + +/** + * 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 that helps us work out what event this + * is when 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 = {}; + +var NS_XHTML = 'http://www.w3.org/1999/xhtml'; + +/** + * Pass-through to createElement or createElementNS + * @param doc The document in which to create the element + * @param tag The name of the tag to create + * @param ns Custom namespace, HTML/XHTML is assumed if this is missing + * @returns The created element + */ +dom.createElement = function(doc, tag, ns) { + // If we've not been given a namespace, but the document is XML, then we + // use an XHTML namespace, otherwise we use HTML + if (ns == null && doc.xmlVersion != null) { + ns = NS_XHTML; + } + if (ns == null) { + return doc.createElement(tag); + } + else { + return doc.createElementNS(ns, tag); + } +}; + +/** + * Remove all the child nodes from this node + * @param el The element that should have it's children removed + */ +dom.clearElement = function(el) { + while (el.hasChildNodes()) { + el.removeChild(el.firstChild); + } +}; + +if (this.document && !this.document.documentElement.classList) { + /** + * Is the given element marked with the given CSS class? + */ + dom.hasCssClass = function(el, name) { + var classes = el.className.split(/\s+/g); + return classes.indexOf(name) !== -1; + }; + + /** + * Add a CSS class to the list of classes on the given node + */ + dom.addCssClass = function(el, name) { + if (!dom.hasCssClass(el, name)) { + el.className += ' ' + name; + } + }; + + /** + * Remove a CSS class from the list of classes on the given node + */ + dom.removeCssClass = function(el, name) { + var classes = el.className.split(/\s+/g); + while (true) { + var index = classes.indexOf(name); + if (index == -1) { + break; + } + classes.splice(index, 1); + } + el.className = classes.join(' '); + }; + + /** + * Add the named CSS class from the element if it is not already present or + * remove it if is present. + */ + dom.toggleCssClass = function(el, name) { + var classes = el.className.split(/\s+/g), add = true; + while (true) { + var index = classes.indexOf(name); + if (index == -1) { + break; + } + add = false; + classes.splice(index, 1); + } + if (add) { + classes.push(name); + } + + el.className = classes.join(' '); + return add; + }; +} else { + /* + * classList shim versions of methods above. + * See the functions above for documentation + */ + dom.hasCssClass = function(el, name) { + return el.classList.contains(name); + }; + + dom.addCssClass = function(el, name) { + el.classList.add(name); + }; + + dom.removeCssClass = function(el, name) { + el.classList.remove(name); + }; + + dom.toggleCssClass = function(el, name) { + return el.classList.toggle(name); + }; +} + +/** + * Add or remove a CSS class from the list of classes on the given node + * depending on the value of include + */ +dom.setCssClass = function(node, className, include) { + if (include) { + dom.addCssClass(node, className); + } else { + dom.removeCssClass(node, className); + } +}; + +/** + * 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; +}; + +/** + * Shim for window.getComputedStyle + */ +dom.computedStyle = function(element, style) { + var win = element.ownerDocument.defaultView; + if (win.getComputedStyle) { + var styles = win.getComputedStyle(element, '') || {}; + return styles[style] || ''; + } + else { + return element.currentStyle[style]; + } +}; + +/** + * Using setInnerHtml(foo) rather than innerHTML = foo allows us to enable + * tweaks in XHTML documents. + */ +dom.setInnerHtml = function(el, html) { + if (!this.document || el.namespaceURI === NS_XHTML) { + try { + dom.clearElement(el); + var range = el.ownerDocument.createRange(); + html = '
' + html + '
'; + el.appendChild(range.createContextualFragment(html)); + } + catch (ex) { + el.innerHTML = html; + } + } + else { + el.innerHTML = html; + } +}; + +/** + * Shim to textarea.selectionStart + */ +dom.getSelectionStart = function(textarea) { + try { + return textarea.selectionStart || 0; + } + catch (e) { + return 0; + } +}; + +/** + * Shim to textarea.selectionStart + */ +dom.setSelectionStart = function(textarea, start) { + return textarea.selectionStart = start; +}; + +/** + * Shim to textarea.selectionEnd + */ +dom.getSelectionEnd = function(textarea) { + try { + return textarea.selectionEnd || 0; + } catch (e) { + return 0; + } +}; + +/** + * Shim to textarea.selectionEnd + */ +dom.setSelectionEnd = function(textarea, end) { + return textarea.selectionEnd = end; +}; + +exports.dom = dom; + + +//------------------------------------------------------------------------------ + +/** + * Various event utilities + */ +var event = {}; + +/** + * Shim for lack of addEventListener on old IE. + */ +event.addListener = function(elem, type, callback) { + if (elem.addEventListener) { + return elem.addEventListener(type, callback, false); + } + if (elem.attachEvent) { + var wrapper = function() { + callback(window.event); + }; + callback._wrapper = wrapper; + elem.attachEvent('on' + type, wrapper); + } +}; + +/** + * Shim for lack of removeEventListener on old IE. + */ +event.removeListener = function(elem, type, callback) { + if (elem.removeEventListener) { + return elem.removeEventListener(type, callback, false); + } + if (elem.detachEvent) { + elem.detachEvent('on' + type, callback._wrapper || callback); + } +}; + +/** + * Prevents propagation and clobbers the default action of the passed event + */ +event.stopEvent = function(e) { + event.stopPropagation(e); + if (e.preventDefault) { + e.preventDefault(); + } + return false; +}; + +/** + * Prevents propagation of the event + */ +event.stopPropagation = function(e) { + if (e.stopPropagation) { + e.stopPropagation(); + } + else { + e.cancelBubble = true; + } +}; + +/** + * 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 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 value to a string representation. + * Where possible, there should be round-tripping between values and their + * string representations. + */ +Type.prototype.stringify = function(value) { + throw new Error('Not implemented'); +}; + +/** + * Convert the given arg to an instance of this type. + * Where possible, there should be round-tripping between values and their + * string representations. + * @param arg An instance of Argument to convert. + * @return Conversion + */ +Type.prototype.parse = function(arg) { + throw new Error('Not implemented'); +}; + +/** + * A convenience method for times when you don't have an argument to parse + * but instead have a string. + * @see #parse(arg) + */ +Type.prototype.parseString = function(str) { + return this.parse(new Argument(str)); +}, + +/** + * The plug-in system, and other things need to know what this type is + * called. The name alone is not enough to fully specify a type. Types like + * 'selection' and 'deferred' need extra data, however this function returns + * only the name, not the extra data. + */ +Type.prototype.name = undefined; + +/** + * If there is some concept of a higher value, return it, + * otherwise return undefined. + */ +Type.prototype.increment = function(value) { + return undefined; +}; + +/** + * If there is some concept of a lower value, return it, + * otherwise return undefined. + */ +Type.prototype.decrement = function(value) { + return undefined; +}; + +/** + * 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 + * replText and the end adjusted to fit. + * @param replText Text to replace the old text value + */ +Argument.prototype.beget = function(replText, options) { + var prefix = this.prefix; + var suffix = this.suffix; + + 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 + * replText and the end adjusted to fit. + * @param replText Text to replace the old text value + */ +ScriptArgument.prototype.beget = function(replText, options) { + var prefix = this.prefix; + var suffix = this.suffix; + + 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: + * + * 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; +}; + +/** + * 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; +}; + +JavascriptType.prototype.parse = function(arg) { + var typed = arg.text; + var scope = globalObject; + + // In FX-land we need to unwrap. TODO: Enable in the browser. + // scope = unwrap(scope); + + // 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 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')); + } + + // TODO: Re-enable this test + // Check if prop is a getter function on obj. Functions can change other + // stuff so we can't execute them to get the next object. Stop here. + // if (isNonNativeGetter(scope, prop)) { + // return new Conversion(typed, arg); + // } + + try { + scope = scope[prop]; + } + catch (ex) { + return new Conversion(typed, arg, Status.ERROR, '' + ex); + } + } + } + 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; + var matches = []; + + for (var prop in scope) { + if (prop.indexOf(matchProp) === 0) { + var value; + try { + value = scope[prop]; + } + catch (ex) { + break; + } + var description; + var incomplete = true; + if (typeof value === 'function') { + description = '(function)'; + } + 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 + ')'; + } + matches.push({ + name: prefix + prop, + value: { + name: prefix + prop, + description: description + }, + incomplete: incomplete + }); + } + if (prop === matchProp) { + status = Status.VALID; + message = ''; + } + } + + // Error message if this isn't valid + if (status !== Status.VALID) { + message = l10n.lookupFormat('jstypeParseMissing', [ matchProp ]); + } + + // If the match is the only one possible, and its VALID, predict nothing + if (matches.length === 1 && status === Status.VALID) { + matches = undefined; + } + else { + // Can we think of a better sort order than alpha? There are certainly some + // properties that are far more commonly used ... + matches.sort(function(p1, p2) { + return p1.name.localeCompare(p2.name); + }); + } + + // More than 10 matches are generally not helpful. We should really do 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 the list of matches + if (matches && matches.length > 10) { + matches = matches.slice(0, 9); + } + + return new Conversion(typed, arg, status, message, matches); +}; + +var ParseState = { + NORMAL: 0, + QUOTE: 2, + DQUOTE: 3 +}; + +var OPEN_BODY = '{[('.split(''); +var CLOSE_BODY = '}])'.split(''); +var OPEN_CLOSE_BODY = { + '{': '}', + '[': ']', + '(': ')' +}; + +/** + * 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; + for (var i = 0; i < text.length; i++) { + c = text[i]; + + 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; + } + } + + 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; +}; + +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/l10n', 'gcli/types'], 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; + + +/** + * 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; +}; + + +/** + * 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) { + console.error(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; + + flashNode(node, 'green'); + + return new Conversion(node, arg, Status.VALID, ''); + } + + Array.prototype.forEach.call(nodes, function(n) { + flashNode(n, 'red'); + }); + + return new Conversion(null, arg, Status.ERROR, + l10n.lookupFormat('nodeParseMultiple', [ nodes.length ])); +}; + +NodeType.prototype.name = 'node'; + + +/** + * 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. + */ +function flashNode(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 createEvent = require('gcli/util').createEvent; + +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. + *

We also record validity information where applicable. + *

For values, null and undefined have distinct definitions. null means + * that a value has been provided, undefined means that it has not. + * Thus, null is a valid default value, and common because it identifies an + * parameter that is optional. undefined means there is no value from + * the command line. + * + *

Events

+ * Assignment publishes the following event:
    + *
  • 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: + * { assignment: ..., conversion: ..., oldConversion: ... } + * @constructor + */ +function Assignment(param, paramIndex) { + this.param = param; + this.paramIndex = paramIndex; + this.assignmentChange = 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 value = conversion.predictions[0]) 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) { + // → is right arrow. We use explicit entities to ensure XML validity + var resultPrefix = '{ ' + args.javascript + ' } → '; + try { + var result = customEval(args.javascript); + + if (result === null) { + return resultPrefix + 'null.'; + } + + if (result === undefined) { + return resultPrefix + 'undefined.'; + } + + if (typeof result === 'function') { + //   is   + return resultPrefix + + (result + '').replace(/\n/g, '
    ').replace(/ /g, ' '); + } + + 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 = 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 = 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. + * + *

    Events

    + *

    Requisition publishes the following events: + *

      + *
    • 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 } + *
    • assignmentChange: This is a forward of the Assignment.assignmentChange + * event. It is fired when any assignment (except the commandAssignment) + * changes. + *
    • inputChange: The text to be mirrored in a command line has changed. + * The event object looks like { newText: X }. + *
    + * + * @param environment An opaque object passed to commands using ExecutionContext + * @param document A DOM Document passed to commands using ExecutionContext in + * order to allow creation of DOM nodes. + * @constructor + */ +function Requisition(environment, document) { + this.environment = environment; + this.document = document; + + // 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 = createEvent('Requisition.assignmentChange'); + this.commandChange = createEvent('Requisition.commandChange'); + this.inputChange = 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 + }); +// this.inputChange(); +}; + +/** + * Assignments have an order, so we need to store them in an array. + * But we also need named access ... + */ +Requisition.prototype.getAssignment = function(nameOrNumber) { + var name = (typeof nameOrNumber === 'string') ? + nameOrNumber : + Object.keys(this._assignments)[nameOrNumber]; + return this._assignments[name]; +}, + +/** + * 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 + var command = this.commandAssignment.getValue(); + 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. + *

    + * The returned object has the following members:

      + *
    • char: The character to which this arg trace refers. + *
    • arg: The Argument to which this character is assigned. + *
    • part: One of ['prefix'|'text'|suffix'] - how was this char understood + *
    + *

    + * The Argument objects are as output from #_tokenize() rather than as applied + * to Assignments by #_assign() (i.e. they are not instances of NamedArgument, + * ArrayArgument, etc). + *

    + * To get at the arguments applied to the assignments simply call + * arg.assignment.arg. If arg.assignment.arg !== arg then + * the arg applied to the assignment will contain the original arg. + * See #_assign() for details. + */ +Requisition.prototype.createInputArgTrace = function() { + if (!this._args) { + throw new Error('createInputMap requires a command line. See source.'); + // If this is a problem then we can fake command line input using + // something like the code in #toCanonicalString(). + } + + var args = []; + 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. + */ +Requisition.prototype.getInputStatusMarkup = function() { + var argTraces = this.createInputArgTrace(); + // We only take a status of INCOMPLETE to be INCOMPLETE when the cursor is + // actually in the argument. Otherwise it's an error. + // 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. + var cursor = this.input.cursor.start === 0 ? + 0 : + this.input.cursor.start - 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.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.environment, this.document); + 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. + *

    The general sequence is: + *

      + *
    • _tokenize(): convert _typed into _parts + *
    • _split(): convert _parts into _command and _unparsedArgs + *
    • _assign(): convert _unparsedArgs into requisition + *
    + */ +Requisition.prototype.update = function(input) { + this.input = input; + if (this.input.cursor == null) { + this.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(environment, document) { + this.environment = environment; + this.document = 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) { + + +/** + * Create an unfulfilled promise + * @constructor + */ +function Promise() { + this._status = Promise.PENDING; + this._value = undefined; + this._onSuccessHandlers = []; + this._onErrorHandlers = []; + + // Debugging help + this._id = Promise._nextId++; + Promise._outstanding[this._id] = this; +}; + +/** + * We give promises and ID so we can track which are outstanding + */ +Promise._nextId = 0; + +/** + * Outstanding promises. Handy list for debugging only. + */ +Promise._outstanding = []; + +/** + * Recently resolved promises. Also for debugging only. + */ +Promise._recent = []; + +/** + * A promise can be in one of 2 states. + * The ERROR and SUCCESS states are terminal, the PENDING state is the only + * start state. + */ +Promise.ERROR = -1; +Promise.PENDING = 0; +Promise.SUCCESS = 1; + +/** + * Yeay for RTTI. + */ +Promise.prototype.isPromise = true; + +/** + * Have we either been resolve()ed or reject()ed? + */ +Promise.prototype.isComplete = function() { + return this._status != Promise.PENDING; +}; + +/** + * Have we resolve()ed? + */ +Promise.prototype.isResolved = function() { + return this._status == Promise.SUCCESS; +}; + +/** + * Have we reject()ed? + */ +Promise.prototype.isRejected = function() { + return this._status == Promise.ERROR; +}; + +/** + * Take the specified action of fulfillment of a promise, and (optionally) + * a different action on promise rejection. + */ +Promise.prototype.then = function(onSuccess, onError) { + if (typeof onSuccess === 'function') { + if (this._status === Promise.SUCCESS) { + onSuccess.call(null, this._value); + } else if (this._status === Promise.PENDING) { + this._onSuccessHandlers.push(onSuccess); + } + } + + if (typeof onError === 'function') { + if (this._status === Promise.ERROR) { + onError.call(null, this._value); + } else if (this._status === Promise.PENDING) { + this._onErrorHandlers.push(onError); + } + } + + return this; +}; + +/** + * Like then() except that rather than returning this we return + * a promise which resolves when the original promise resolves. + */ +Promise.prototype.chainPromise = function(onSuccess) { + var chain = new Promise(); + chain._chainedFrom = this; + this.then(function(data) { + try { + chain.resolve(onSuccess(data)); + } catch (ex) { + chain.reject(ex); + } + }, function(ex) { + chain.reject(ex); + }); + return chain; +}; + +/** + * Supply the fulfillment of a promise + */ +Promise.prototype.resolve = function(data) { + return this._complete(this._onSuccessHandlers, Promise.SUCCESS, data, 'resolve'); +}; + +/** + * Renege on a promise + */ +Promise.prototype.reject = function(data) { + return this._complete(this._onErrorHandlers, Promise.ERROR, data, 'reject'); +}; + +/** + * Internal method to be called on resolve() or reject(). + * @private + */ +Promise.prototype._complete = function(list, status, data, name) { + // Complain if we've already been completed + if (this._status != Promise.PENDING) { + console.group('Promise already closed'); + console.error('Attempted ' + name + '() with ', data); + console.error('Previous status = ', this._status, + ', previous value = ', this._value); + console.trace(); + + console.groupEnd(); + return this; + } + + this._status = status; + this._value = data; + + // Call all the handlers, and then delete them + list.forEach(function(handler) { + handler.call(null, this._value); + }, this); + delete this._onSuccessHandlers; + delete this._onErrorHandlers; + + // Remove the given {promise} from the _outstanding list, and add it to the + // _recent list, pruning more than 20 recent promises from that list. + delete Promise._outstanding[this._id]; + Promise._recent.push(this); + while (Promise._recent.length > 20) { + Promise._recent.shift(); + } + + return this; +}; + +/** + * Takes an array of promises and returns a promise that that is fulfilled once + * all the promises in the array are fulfilled + * @param promiseList The array of promises + * @return the promise that is fulfilled when all the array is fulfilled + */ +Promise.group = function(promiseList) { + if (!(promiseList instanceof Array)) { + promiseList = Array.prototype.slice.call(arguments); + } + + // If the original array has nothing in it, return now to avoid waiting + if (promiseList.length === 0) { + return new Promise().resolve([]); + } + + var groupPromise = new Promise(); + var results = []; + var fulfilled = 0; + + var onSuccessFactory = function(index) { + return function(data) { + results[index] = data; + fulfilled++; + // If the group has already failed, silently drop extra results + if (groupPromise._status !== Promise.ERROR) { + if (fulfilled === promiseList.length) { + groupPromise.resolve(results); + } + } + }; + }; + + promiseList.forEach(function(promise, index) { + var onSuccess = onSuccessFactory(index); + var onError = groupPromise.reject.bind(groupPromise); + promise.then(onSuccess, onError); + }); + + return groupPromise; +}; + +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/ui/inputter', ['require', 'exports', 'module' , 'gcli/util', 'gcli/types', 'gcli/history', 'text!gcli/ui/inputter.css'], function(require, exports, module) { +var cliView = exports; + + +var event = require('gcli/util').event; +var dom = require('gcli/util').dom; +var KeyEvent = event.KeyEvent; + +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; + + // Suss out where the input element is + this.element = options.inputElement || 'gcliInput'; + 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); + event.addListener(this.element, 'keydown', this.onKeyDown); + event.addListener(this.element, 'keyup', this.onKeyUp); + + if (options.completer == null) { + options.completer = new Completer(options); + } + else if (typeof options.completer === 'function') { + options.completer = new options.completer(options); + } + this.completer = options.completer; + this.completer.decorate(this); + + // Use the provided history object, or instantiate our own + this.history = options.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); + event.addListener(this.element, 'mouseup', this.onMouseUp); + + this.focusManager = options.focusManager; + if (this.focusManager) { + this.focusManager.addMonitoredElement(this.element, 'input'); + } + + this.requisition.inputChange.add(this.onInputChange, this); +} + +/** + * Avoid memory leaks + */ +Inputter.prototype.destroy = function() { + this.requisition.inputChange.remove(this.onInputChange, this); + if (this.focusManager) { + this.focusManager.removeMonitoredElement(this.element, 'input'); + } + + event.removeListener(this.element, 'keydown', this.onKeyDown); + event.removeListener(this.element, 'keyup', this.onKeyUp); + 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); + } + + dom.setSelectionStart(this.element, newInput.cursor.start); + dom.setSelectionEnd(this.element, 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) { + event.stopEvent(ev); + } + if (ev.keyCode === KeyEvent.DOM_VK_TAB) { + this.lastTabDownAt = 0; + if (!ev.shiftKey) { + event.stopEvent(ev); + // 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) { + // 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) { + this.getCurrentAssignment().complete(); + // 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 + this._caretChange = Caret.TO_ARG_END; + this._processCaretChange(this.getInputState(), true); + } + 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 = dom.getSelectionStart(this.element); + 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: dom.getSelectionStart(this.element), + end: dom.getSelectionEnd(this.element) + } + }; + + // Workaround for potential XUL bug 676520 where textbox gives incorrect + // values for its content + if (input.typed == null) { + input.typed = ''; + console.log('fixing input.typed=""', input); + } + + 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 to show before a completion. + * Defaults to '»' (double greater-than, a.k.a right guillemet). + */ +function Completer(options) { + this.document = options.document; + this.requisition = options.requisition; + this.elementCreated = false; + + this.element = options.completeElement || 'gcliComplete'; + 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 = 'gcliCompletion gcliVALID'; + this.element.setAttribute('tabindex', '-1'); + this.element.setAttribute('aria-live', 'polite'); + } + } + + this.completionPrompt = typeof options.completionPrompt === 'string' + ? options.completionPrompt + : '»'; + + 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) { + event.removeListener(this.document.defaultView, 'resize', this.resizer); + } + + 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); + + Completer.copyStyles.forEach(function(style) { + this.element.style[style] = dom.computedStyle(input, 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); + event.addListener(this.document.defaultView, 'resize', this.resizer); + this.resizer(); + } +}; + +/** + * Ensure that the completion element is the same size and the inputter element + */ +Completer.prototype.resizer = function() { + 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(); + + var completion = '' + this.completionPrompt + ' '; + if (input.typed.length > 0) { + var scores = this.requisition.getInputStatusMarkup(); + completion += this.markupStatusScore(scores, input); + } + + if (input.typed.length > 0 && predictions.length > 0) { + var tab = predictions[0].name; + var existing = current.getArg().text; + 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; + var suffix = tab.slice(existing.length - numLeadingSpaces); + completion += '' + suffix + ''; + } else { + // Display the '-> prediction' at the end of the completer element + completion += '  ⇥ ' + + tab + ''; + } + } + + // A hack to 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(); + if (command && command.name === '{') { + if (this.requisition.getAssignment(0).getArg().suffix.indexOf('}') === -1) { + completion += '}'; + } + } + + dom.setInnerHtml(this.element, '' + completion + ''); +}; + +/** + * Mark-up an array of Status values with spans + */ +Completer.prototype.markupStatusScore = function(scores, input) { + var completion = ''; + if (scores.length === 0) { + return completion; + } + + var i = 0; + var lastStatus = -1; + 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; + } + completion += ''; + lastStatus = scores[i]; + } + var char = input.typed[i]; + if (char === ' ') { + char = ' '; + } + completion += char; + i++; + if (i === input.typed.length) { + completion += ''; + break; + } + if (lastStatus !== scores[i]) { + completion += ''; + } + } + + return completion; +}; + +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", [], void 0); +/* + * 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 Templater = require('gcli/ui/domtemplate').Templater; + +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 document The document to use in creating widgets + * @param requisition The Requisition to fill out + */ +function ArgFetcher(document, requisition) { + this.document = document; + this.requisition = 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 = 'gcliCliEle'; + // We cache the fields we create so we can destroy them later + this.fields = []; + + this.tmpl = new Templater(); + // 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('#gcliReqTempl'); + + this.requisition.commandChange.add(this.onCommandChange, this); + this.requisition.inputChange.add(this.onInputChange, this); +} + +/** + * 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); + this.tmpl.processNode(reqEle, this); + 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) { + 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; +}; + +/** + * 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(); +}; + +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.style.width = '100%'; + + 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. + *

    There are 3 slightly overlapping concepts to be aware of: + *

      + *
    • value: This is the (probably non-string) value, known as a value by the + * assignment + *
    • optValue: This is the text value as known by the DOM option element, as + * in <option value=???%gt... + *
    • optText: This is the contents of the DOM option element. + *
    + */ +function SelectionField(type, options) { + this.document = options.document; + this.type = type; + this.items = []; + + this.element = dom.createElement(this.document, 'select'); + this.element.style.width = '180px'; + 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.style.marginBottom = '0px'; + this.input.style.width = options.name.length === 0 ? '240px' : '160px'; + this.element.appendChild(this.input); + + this.menu = new Menu(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); + if (conversion.getStatus() === Status.ERROR) { + 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 = []; + + //
    + this.element = dom.createElement(this.document, 'div'); + this.element.className = 'gcliArrayParent'; + + //