/* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ "use strict"; /** * DO NOT MODIFY THIS FILE DIRECTLY. * This file is generated from separate files stored in the GCLI project. * Please modify the files there and use the import script so the 2 projects * are kept in sync. * For more information, ask Joe Walker */ var EXPORTED_SYMBOLS = [ "gcli" ]; Components.utils.import("resource://gre/modules/devtools/Require.jsm"); Components.utils.import("resource://gre/modules/devtools/Console.jsm"); Components.utils.import("resource:///modules/devtools/Browser.jsm"); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var mozl10n = {}; (function(aMozl10n) { var temp = {}; Components.utils.import("resource://gre/modules/Services.jsm", temp); var stringBundle = temp.Services.strings.createBundle( "chrome://browser/locale/devtools/gclicommands.properties"); /** * Lookup a string in the GCLI string bundle * @param name The name to lookup * @return The looked up name */ aMozl10n.lookup = function(name) { try { return stringBundle.GetStringFromName(name); } catch (ex) { throw new Error("Failure in lookup('" + name + "')"); } }; /** * Lookup a string in the GCLI string bundle * @param name The name to lookup * @param swaps An array of swaps. See stringBundle.formatStringFromName * @return The looked up name */ aMozl10n.lookupFormat = function(name, swaps) { try { return stringBundle.formatStringFromName(name, swaps, swaps.length); } catch (ex) { throw new Error("Failure in lookupFormat('" + name + "')"); } }; })(mozl10n); define('gcli/index', ['require', 'exports', 'module' , 'gcli/types/basic', 'gcli/types/command', 'gcli/types/javascript', 'gcli/types/node', 'gcli/types/resource', 'gcli/types/setting', 'gcli/types/selection', 'gcli/settings', 'gcli/ui/intro', 'gcli/ui/focus', 'gcli/ui/fields/basic', 'gcli/ui/fields/javascript', 'gcli/ui/fields/selection', 'gcli/commands/help', 'gcli/commands/pref', 'gcli/canon', 'gcli/ui/ffdisplay'], function(require, exports, module) { // Internal startup process. Not exported require('gcli/types/basic').startup(); require('gcli/types/command').startup(); require('gcli/types/javascript').startup(); require('gcli/types/node').startup(); require('gcli/types/resource').startup(); require('gcli/types/setting').startup(); require('gcli/types/selection').startup(); require('gcli/settings').startup(); require('gcli/ui/intro').startup(); require('gcli/ui/focus').startup(); require('gcli/ui/fields/basic').startup(); require('gcli/ui/fields/javascript').startup(); require('gcli/ui/fields/selection').startup(); require('gcli/commands/help').startup(); require('gcli/commands/pref').startup(); // The API for use by command authors exports.addCommand = require('gcli/canon').addCommand; exports.removeCommand = require('gcli/canon').removeCommand; exports.lookup = mozl10n.lookup; exports.lookupFormat = mozl10n.lookupFormat; /** * This code is internal and subject to change without notice. * createView() for Firefox requires an options object with the following * members: * - contentDocument: From the window of the attached tab * - chromeDocument: GCLITerm.document * - environment.hudId: GCLITerm.hudId * - jsEnvironment.globalObject: 'window' * - jsEnvironment.evalFunction: 'eval' in a sandbox * - inputElement: GCLITerm.inputNode * - completeElement: GCLITerm.completeNode * - hintElement: GCLITerm.hintNode * - inputBackgroundElement: GCLITerm.inputStack */ exports.createDisplay = function(opts) { var FFDisplay = require('gcli/ui/ffdisplay').FFDisplay; return new FFDisplay(opts); }; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/types/basic', ['require', 'exports', 'module' , 'gcli/l10n', 'gcli/types', 'gcli/types/selection', 'gcli/argument'], function(require, exports, module) { var l10n = require('gcli/l10n'); var types = require('gcli/types'); var Type = require('gcli/types').Type; var Status = require('gcli/types').Status; var Conversion = require('gcli/types').Conversion; var ArrayConversion = require('gcli/types').ArrayConversion; var SelectionType = require('gcli/types/selection').SelectionType; var BlankArgument = require('gcli/argument').BlankArgument; var ArrayArgument = require('gcli/argument').ArrayArgument; /** * Registration and de-registration. */ exports.startup = function() { types.registerType(StringType); types.registerType(NumberType); types.registerType(BooleanType); types.registerType(BlankType); types.registerType(DeferredType); types.registerType(ArrayType); }; exports.shutdown = function() { types.unregisterType(StringType); types.unregisterType(NumberType); types.unregisterType(BooleanType); types.unregisterType(BlankType); types.unregisterType(DeferredType); types.unregisterType(ArrayType); }; /** * 'string' the most basic string type that doesn't need to convert */ function StringType(typeSpec) { } StringType.prototype = Object.create(Type.prototype); StringType.prototype.stringify = function(value) { if (value == null) { return ''; } return value.toString(); }; StringType.prototype.parse = function(arg) { if (arg.text == null || arg.text === '') { return new Conversion(undefined, arg, Status.INCOMPLETE, ''); } return new Conversion(arg.text, arg); }; StringType.prototype.name = 'string'; exports.StringType = StringType; /** * We don't currently plan to distinguish between integers and floats */ function NumberType(typeSpec) { if (typeSpec) { this._min = typeSpec.min; this._max = typeSpec.max; this._step = typeSpec.step || 1; } else { this._step = 1; } } NumberType.prototype = Object.create(Type.prototype); NumberType.prototype.stringify = function(value) { if (value == null) { return ''; } return '' + value; }; NumberType.prototype.getMin = function() { if (this._min) { if (typeof this._min === 'function') { return this._min(); } if (typeof this._min === 'number') { return this._min; } } return undefined; }; NumberType.prototype.getMax = function() { if (this._max) { if (typeof this._max === 'function') { return this._max(); } if (typeof this._max === 'number') { return this._max; } } return undefined; }; NumberType.prototype.parse = function(arg) { if (arg.text.replace(/^\s*-?/, '').length === 0) { return new Conversion(undefined, arg, Status.INCOMPLETE, ''); } var value = parseInt(arg.text, 10); if (isNaN(value)) { return new Conversion(undefined, arg, Status.ERROR, l10n.lookupFormat('typesNumberNan', [ arg.text ])); } var max = this.getMax(); if (max != null && value > max) { return new Conversion(undefined, arg, Status.ERROR, l10n.lookupFormat('typesNumberMax', [ value, max ])); } var min = this.getMin(); if (min != null && value < min) { return new Conversion(undefined, arg, Status.ERROR, l10n.lookupFormat('typesNumberMin', [ value, min ])); } return new Conversion(value, arg); }; NumberType.prototype.decrement = function(value) { if (typeof value !== 'number' || isNaN(value)) { return this.getMax() || 1; } var newValue = value - this._step; // Snap to the nearest incremental of the step newValue = Math.ceil(newValue / this._step) * this._step; return this._boundsCheck(newValue); }; NumberType.prototype.increment = function(value) { if (typeof value !== 'number' || isNaN(value)) { var min = this.getMin(); return min != null ? min : 0; } var newValue = value + this._step; // Snap to the nearest incremental of the step newValue = Math.floor(newValue / this._step) * this._step; if (this.getMax() == null) { return newValue; } return this._boundsCheck(newValue); }; /** * Return the input value so long as it is within the max/min bounds. If it is * lower than the minimum, return the minimum. If it is bigger than the maximum * then return the maximum. */ NumberType.prototype._boundsCheck = function(value) { var min = this.getMin(); if (min != null && value < min) { return min; } var max = this.getMax(); if (max != null && value > max) { return max; } return value; }; NumberType.prototype.name = 'number'; exports.NumberType = NumberType; /** * true/false values */ function BooleanType(typeSpec) { } BooleanType.prototype = Object.create(SelectionType.prototype); BooleanType.prototype.lookup = [ { name: 'false', value: false }, { name: 'true', value: true } ]; BooleanType.prototype.parse = function(arg) { if (arg.type === 'TrueNamedArgument') { return new Conversion(true, arg); } if (arg.type === 'FalseNamedArgument') { return new Conversion(false, arg); } return SelectionType.prototype.parse.call(this, arg); }; BooleanType.prototype.stringify = function(value) { if (value == null) { return ''; } return '' + value; }; BooleanType.prototype.getBlank = function() { return new Conversion(false, new BlankArgument(), Status.VALID, '', this.lookup); }; BooleanType.prototype.name = 'boolean'; exports.BooleanType = BooleanType; /** * A type for "we don't know right now, but hope to soon". */ function DeferredType(typeSpec) { if (typeof typeSpec.defer !== 'function') { throw new Error('Instances of DeferredType need typeSpec.defer to be a function that returns a type'); } Object.keys(typeSpec).forEach(function(key) { this[key] = typeSpec[key]; }, this); } DeferredType.prototype = Object.create(Type.prototype); DeferredType.prototype.stringify = function(value) { return this.defer().stringify(value); }; DeferredType.prototype.parse = function(arg) { return this.defer().parse(arg); }; DeferredType.prototype.decrement = function(value) { var deferred = this.defer(); return (deferred.decrement ? deferred.decrement(value) : undefined); }; DeferredType.prototype.increment = function(value) { var deferred = this.defer(); return (deferred.increment ? deferred.increment(value) : undefined); }; DeferredType.prototype.increment = function(value) { var deferred = this.defer(); return (deferred.increment ? deferred.increment(value) : undefined); }; DeferredType.prototype.getType = function() { return this.defer(); }; Object.defineProperty(DeferredType.prototype, 'isImportant', { get: function() { return this.defer().isImportant; }, enumerable: true }); DeferredType.prototype.name = 'deferred'; exports.DeferredType = DeferredType; /** * 'blank' is a type for use with DeferredType when we don't know yet. * It should not be used anywhere else. */ function BlankType(typeSpec) { } BlankType.prototype = Object.create(Type.prototype); BlankType.prototype.stringify = function(value) { return ''; }; BlankType.prototype.parse = function(arg) { return new Conversion(undefined, arg); }; BlankType.prototype.name = 'blank'; exports.BlankType = BlankType; /** * A set of objects of the same type */ function ArrayType(typeSpec) { if (!typeSpec.subtype) { console.error('Array.typeSpec is missing subtype. Assuming string.' + JSON.stringify(typeSpec)); typeSpec.subtype = 'string'; } Object.keys(typeSpec).forEach(function(key) { this[key] = typeSpec[key]; }, this); this.subtype = types.getType(this.subtype); } ArrayType.prototype = Object.create(Type.prototype); ArrayType.prototype.stringify = function(values) { if (values == null) { return ''; } // BUG 664204: Check for strings with spaces and add quotes return values.join(' '); }; ArrayType.prototype.parse = function(arg) { if (arg.type === 'ArrayArgument') { var conversions = arg.getArguments().map(function(subArg) { var conversion = this.subtype.parse(subArg); // Hack alert. ArrayConversion needs to be able to answer questions // about the status of individual conversions in addition to the // overall state. This allows us to do that easily. subArg.conversion = conversion; return conversion; }, this); return new ArrayConversion(conversions, arg); } else { console.error('non ArrayArgument to ArrayType.parse', arg); throw new Error('non ArrayArgument to ArrayType.parse'); } }; ArrayType.prototype.getBlank = function(values) { return new ArrayConversion([], new ArrayArgument()); }; ArrayType.prototype.name = 'array'; exports.ArrayType = ArrayType; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 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'); var imports = {}; XPCOMUtils.defineLazyGetter(imports, 'stringBundle', function () { return Services.strings.createBundle('chrome://browser/locale/devtools/gcli.properties'); }); /* * Not supported when embedded - we're doing things the Mozilla way not the * require.js way. */ exports.registerStringsSource = function(modulePath) { throw new Error('registerStringsSource is not available in mozilla'); }; exports.unregisterStringsSource = function(modulePath) { throw new Error('unregisterStringsSource is not available in mozilla'); }; exports.lookupSwap = function(key, swaps) { throw new Error('lookupSwap is not available in mozilla'); }; exports.lookupPlural = function(key, ord, swaps) { throw new Error('lookupPlural is not available in mozilla'); }; exports.getPreferredLocales = function() { return [ 'root' ]; }; /** @see lookup() in lib/gcli/l10n.js */ exports.lookup = function(key) { try { // Our memory leak hunter walks reachable objects trying to work out what // type of thing they are using object.constructor.name. If that causes // problems then we can avoid the unknown-key-exception with the following: /* if (key === 'constructor') { return { name: 'l10n-mem-leak-defeat' }; } */ return imports.stringBundle.GetStringFromName(key); } catch (ex) { console.error('Failed to lookup ', key, ex); return key; } }; /** @see propertyLookup in lib/gcli/l10n.js */ exports.propertyLookup = Proxy.create({ get: function(rcvr, name) { return exports.lookup(name); } }); /** @see lookupFormat in lib/gcli/l10n.js */ exports.lookupFormat = function(key, swaps) { try { return imports.stringBundle.formatStringFromName(key, swaps, swaps.length); } catch (ex) { console.error('Failed to format ', key, ex); return key; } }; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/types', ['require', 'exports', 'module' , 'gcli/argument'], function(require, exports, module) { var Argument = require('gcli/argument').Argument; var BlankArgument = require('gcli/argument').BlankArgument; /** * 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; } }; exports.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; } /** * Ensure that all arguments that are part of this conversion know what they * are assigned to. * @param assignment The Assignment (param/conversion link) to inform the * argument about. */ Conversion.prototype.assign = function(assignment) { this.arg.assign(assignment); }; /** * Work out if there is information provided in the contained argument. */ Conversion.prototype.isDataProvided = function() { return this.arg.type !== 'BlankArgument'; }; /** * 2 conversions are equal if and only if their args are equal (argEquals) and * their values are equal (valueEquals). * @param that The conversion object to compare against. */ Conversion.prototype.equals = function(that) { if (this === that) { return true; } if (that == null) { return false; } return this.valueEquals(that) && this.argEquals(that); }; /** * Check that the value in this conversion is strict equal to the value in the * provided conversion. * @param that The conversion to compare values with */ Conversion.prototype.valueEquals = function(that) { return this.value === that.value; }; /** * Check that the argument in this conversion is equal to the value in the * provided conversion as defined by the argument (i.e. arg.equals). * @param that The conversion to compare arguments with */ Conversion.prototype.argEquals = function(that) { return that == null ? false : this.arg.equals(that.arg); }; /** * Accessor for the status of this conversion */ Conversion.prototype.getStatus = function(arg) { return this._status; }; /** * Defined by the toString() value provided by the argument */ Conversion.prototype.toString = function() { return this.arg.toString(); }; /** * If status === INCOMPLETE, then we may be able to provide predictions as to * how the argument can be completed. * @return An array of items, where each item is an object with the following * properties: * - name (mandatory): Displayed to the user, and typed in. No whitespace * - description (optional): Short string for display in a tool-tip * - manual (optional): Longer description which details usage * - incomplete (optional): Indicates that the prediction if used should not * be considered necessarily sufficient, which typically will mean that the * UI should not append a space to the completion * - value (optional): If a value property is present, this will be used as the * value of the conversion, otherwise the item itself will be used. */ Conversion.prototype.getPredictions = function() { if (typeof this.predictions === 'function') { return this.predictions(); } return this.predictions || []; }; /** * Accessor for a prediction by index. * This is useful above getPredictions()[index] because it normalizes * index to be within the bounds of the predictions, which means that the UI * can maintain an index of which prediction to choose without caring how many * predictions there are. * @param index The index of the prediction to choose */ Conversion.prototype.getPredictionAt = function(index) { if (index == null) { return undefined; } var predictions = this.getPredictions(); if (predictions.length === 0) { return undefined; } index = index % predictions.length; if (index < 0) { index = predictions.length + index; } return predictions[index]; }; /** * Accessor for a prediction by index. * This is useful above getPredictions()[index] because it normalizes * index to be within the bounds of the predictions, which means that the UI * can maintain an index of which prediction to choose without caring how many * predictions there are. * @param index The index of the prediction to choose */ Conversion.prototype.constrainPredictionIndex = function(index) { if (index == null) { return undefined; } var predictions = this.getPredictions(); if (predictions.length === 0) { return undefined; } index = index % predictions.length; if (index < 0) { index = predictions.length + index; } return index; }; /** * Constant to allow everyone to agree on the maximum number of predictions * that should be provided. We actually display 1 less than this number. */ Conversion.maxPredictions = 11; exports.Conversion = Conversion; /** * 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(', ') + ' ]'; }; exports.ArrayConversion = ArrayConversion; /** * Most of our types are 'static' e.g. there is only one type of 'string', * however some types like 'selection' and 'deferred' are customizable. * The basic Type type isn't useful, but does provide documentation about what * types do. */ function Type() { } /** * Convert the given value to a string representation. * Where possible, there should be round-tripping between values and their * string representations. */ Type.prototype.stringify = function(value) { throw new Error('Not implemented'); }; /** * Convert the given arg to an instance of this type. * Where possible, there should be round-tripping between values and their * string representations. * @param arg An instance of Argument to convert. * @return Conversion */ Type.prototype.parse = function(arg) { throw new Error('Not implemented'); }; /** * A convenience method for times when you don't have an argument to parse * but instead have a string. * @see #parse(arg) */ Type.prototype.parseString = function(str) { return this.parse(new Argument(str)); }, /** * The plug-in system, and other things need to know what this type is * called. The name alone is not enough to fully specify a type. Types like * 'selection' and 'deferred' need extra data, however this function returns * only the name, not the extra data. */ Type.prototype.name = undefined; /** * If there is some concept of a higher value, return it, * otherwise return undefined. */ Type.prototype.increment = function(value) { return undefined; }; /** * If there is some concept of a lower value, return it, * otherwise return undefined. */ Type.prototype.decrement = function(value) { return undefined; }; /** * The 'blank value' of most types is 'undefined', but there are exceptions; * This allows types to specify a better conversion from empty string than * 'undefined'. * 2 known examples of this are boolean -> false and array -> [] */ Type.prototype.getBlank = function() { return this.parse(new BlankArgument()); }; /** * This is something of a hack for the benefit of DeferredType which needs to * be able to lie about it's type for fields to accept it as one of their own. * Sub-types can ignore this unless they're DeferredType. */ Type.prototype.getType = function() { return this; }; exports.Type = Type; /** * Private registry of types * Invariant: types[name] = type.name */ var registeredTypes = {}; exports.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. */ exports.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); } }; exports.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 */ exports.deregisterType = function(type) { delete registeredTypes[type.name]; }; /** * Find a type, previously registered using #registerType() */ exports.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 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/argument', ['require', 'exports', 'module' ], function(require, exports, module) { var argument = exports; /** * Thinking out loud here: * Arguments are an area where we could probably refactor things a bit better. * The split process in Requisition creates a set of Arguments, which are then * assigned. The assign process sometimes converts them into subtypes of * Argument. We might consider that what gets assigned is _always_ one of the * subtypes (or actually a different type hierarchy entirely) and that we * don't manipulate the prefix/text/suffix but just use the 'subtypes' as * filters which present a view of the underlying original Argument. */ /** * 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 : ''; } } Argument.prototype.type = 'Argument'; /** * Return the result of merging these arguments. * case and some of the arguments are in quotation marks? */ Argument.prototype.merge = function(following) { // Is it possible that this gets called when we're merging arguments // for the single string? return new Argument( this.text + this.suffix + following.prefix + following.text, this.prefix, following.suffix); }; /** * Returns a new Argument like this one but with the text set to * replText and the end adjusted to fit. * @param replText Text to replace the old text value */ Argument.prototype.beget = function(replText, options) { var prefix = this.prefix; var suffix = this.suffix; // We need to add quotes when the replacement string has spaces or is empty var quote = (replText.indexOf(' ') >= 0 || replText.length == 0) ? '\'' : ''; if (options) { prefix = (options.prefixSpace ? ' ' : '') + quote; suffix = quote; } return new Argument(replText, prefix, suffix); }; /** * 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; }; /** * For test/debug use only. The output from this function is subject to wanton * random change without notice, and should not be relied upon to even exist * at some later date. */ Object.defineProperty(Argument.prototype, '_summaryJson', { get: function() { var assignStatus = this.assignment == null ? 'null' : this.assignment.param.name; return '<' + this.prefix + ':' + this.text + ':' + this.suffix + '>' + ' (a=' + assignStatus + ',' + ' t=' + this.type + ')'; }, enumerable: true }); argument.Argument = Argument; /** * BlankArgument is a marker that the argument wasn't typed but is there to * fill a slot. Assignments begin with their arg set to a BlankArgument. */ function BlankArgument() { this.text = ''; this.prefix = ''; this.suffix = ''; } BlankArgument.prototype = Object.create(Argument.prototype); BlankArgument.prototype.type = 'BlankArgument'; argument.BlankArgument = BlankArgument; /** * ScriptArgument is a marker that the argument is designed to be Javascript. * It also implements the special rules that spaces after the { or before the * } are part of the pre/suffix rather than the content, and that they are * never 'blank' so they can be used by Requisition._split() and not raise an * ERROR status due to being blank. */ function ScriptArgument(text, prefix, suffix) { this.text = text !== undefined ? text : ''; this.prefix = prefix !== undefined ? prefix : ''; this.suffix = suffix !== undefined ? suffix : ''; while (this.text.charAt(0) === ' ') { this.prefix = this.prefix + ' '; this.text = this.text.substring(1); } while (this.text.charAt(this.text.length - 1) === ' ') { this.suffix = ' ' + this.suffix; this.text = this.text.slice(0, -1); } } ScriptArgument.prototype = Object.create(Argument.prototype); ScriptArgument.prototype.type = 'ScriptArgument'; /** * Returns a new Argument like this one but with the text set to * replText and the end adjusted to fit. * @param replText Text to replace the old text value */ ScriptArgument.prototype.beget = function(replText, options) { var prefix = this.prefix; var suffix = this.suffix; if (options && options.normalize) { prefix = '{ '; suffix = ' }'; } return new ScriptArgument(replText, prefix, suffix); }; 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); MergedArgument.prototype.type = 'MergedArgument'; /** * 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.type = 'TrueNamedArgument'; 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.type = 'FalseNamedArgument'; 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: * * We model this as a normal argument but with a long prefix. */ function NamedArgument(nameArg, valueArg) { this.nameArg = nameArg; this.valueArg = valueArg; if (valueArg == null) { this.text = ''; this.prefix = nameArg.toString(); this.suffix = ''; } else { this.text = valueArg.text; this.prefix = nameArg.toString() + valueArg.prefix; this.suffix = valueArg.suffix; } } NamedArgument.prototype = Object.create(Argument.prototype); NamedArgument.prototype.type = 'NamedArgument'; NamedArgument.prototype.assign = function(assignment) { this.nameArg.assign(assignment); if (this.valueArg != null) { 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.type = 'ArrayArgument'; 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.type === '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 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/types/selection', ['require', 'exports', 'module' , 'gcli/l10n', 'gcli/types', 'gcli/types/spell'], function(require, exports, module) { var l10n = require('gcli/l10n'); var types = require('gcli/types'); var Type = require('gcli/types').Type; var Status = require('gcli/types').Status; var Conversion = require('gcli/types').Conversion; var Speller = require('gcli/types/spell').Speller; /** * Registration and de-registration. */ exports.startup = function() { types.registerType(SelectionType); }; exports.shutdown = function() { types.unregisterType(SelectionType); }; /** * A selection allows the user to pick a value from known set of options. * An option is made up of a name (which is what the user types) and a value * (which is passed to exec) * @param typeSpec Object containing properties that describe how this * selection functions. Properties include: * - lookup: An array of objects, one for each option, which contain name and * value properties. lookup can be a function which returns this array * - data: An array of strings - alternative to 'lookup' where the valid values * are strings. i.e. there is no mapping between what is typed and the value * that is used by the program * - stringifyProperty: Conversion from value to string is generally a process * of looking through all the valid options for a matching value, and using * the associated name. However the name maybe available directly from the * value using a property lookup. Setting 'stringifyProperty' allows * SelectionType to take this shortcut. * - cacheable: If lookup is a function, then we normally assume that * the values fetched can change. Setting 'cacheable:true' enables internal * caching. */ 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) { if (value == null) { return ''; } if (this.stringifyProperty != null) { return value[this.stringifyProperty]; } var name = null; var lookup = this.getLookup(); lookup.some(function(item) { if (item.value === value) { name = item.name; return true; } return false; }, this); return name; }; /** * If typeSpec contained cacheable:true then calls to parse() work on cached * data. clearCache() enables the cache to be cleared. */ SelectionType.prototype.clearCache = function() { delete this._cachedLookup; }; /** * There are several ways to get selection data. This unifies them into one * single function. * @return An array of objects with name and value properties. */ SelectionType.prototype.getLookup = function() { if (this._cachedLookup) { return this._cachedLookup; } if (this.lookup) { if (typeof this.lookup === 'function') { if (this.cacheable) { this._cachedLookup = this.lookup(); return this._cachedLookup; } return this.lookup(); } return this.lookup; } if (Array.isArray(this.data)) { this.lookup = this._dataToLookup(this.data); return this.lookup; } if (typeof(this.data) === 'function') { return this._dataToLookup(this.data()); } throw new Error('SelectionType has no data'); }; /** * Selection can be provided with either a lookup object (in the 'lookup' * property) or an array of strings (in the 'data' property). Internally we * always use lookup, so we need a way to convert a 'data' array to a lookup. */ SelectionType.prototype._dataToLookup = function(data) { return data.map(function(option) { return { name: option, value: option }; }, this); }; /** * Return a list of possible completions for the given arg. * @param arg The initial input to match * @return A trimmed array of string:value pairs */ SelectionType.prototype._findPredictions = function(arg) { var predictions = []; var lookup = this.getLookup(); var i, option; var maxPredictions = Conversion.maxPredictions; var match = arg.text.toLowerCase(); // If the arg has a suffix then we're kind of 'done'. Only an exact match // will do. if (arg.suffix.length > 0) { for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) { option = lookup[i]; if (option.name === arg.text) { this._addToPredictions(predictions, option, arg); } } return predictions; } // Cache lower case versions of all the option names for (i = 0; i < lookup.length; i++) { option = lookup[i]; if (option._gcliLowerName == null) { option._gcliLowerName = option.name.toLowerCase(); } } // Start with prefix matching for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) { option = lookup[i]; if (option._gcliLowerName.indexOf(match) === 0) { this._addToPredictions(predictions, option, arg); } } // Try infix matching if we get less half max matched if (predictions.length < (maxPredictions / 2)) { for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) { option = lookup[i]; if (option._gcliLowerName.indexOf(match) !== -1) { if (predictions.indexOf(option) === -1) { this._addToPredictions(predictions, option, arg); } } } } // Try fuzzy matching if we don't get a prefix match if (false && predictions.length === 0) { var speller = new Speller(); var names = lookup.map(function(opt) { return opt.name; }); speller.train(names); var corrected = speller.correct(match); if (corrected) { lookup.forEach(function(opt) { if (opt.name === corrected) { predictions.push(opt); } }, this); } } return predictions; }; /** * Add an option to our list of predicted options. * We abstract out this portion of _findPredictions() because CommandType needs * to make an extra check before actually adding which SelectionType does not * need to make. */ SelectionType.prototype._addToPredictions = function(predictions, option, arg) { predictions.push(option); }; SelectionType.prototype.parse = function(arg) { var predictions = this._findPredictions(arg); if (predictions.length === 0) { var msg = l10n.lookupFormat('typesSelectionNomatch', [ arg.text ]); return new Conversion(undefined, arg, Status.ERROR, msg, predictions); } // This is something of a hack it basically allows us to tell the // setting type to forget its last setting hack. if (this.noMatch) { this.noMatch(); } if (predictions[0].name === arg.text) { var value = predictions[0].value; return new Conversion(value, arg, Status.VALID, '', predictions); } return new Conversion(undefined, arg, Status.INCOMPLETE, '', predictions); }; /** * For selections, up is down and black is white. It's like this, given a list * [ a, b, c, d ], it's natural to think that it starts at the top and that * going up the list, moves towards 'a'. However 'a' has the lowest index, so * for SelectionType, up is down and down is up. * Sorry. */ SelectionType.prototype.decrement = function(value) { var lookup = this.getLookup(); var index = this._findValue(lookup, value); if (index === -1) { index = 0; } index++; if (index >= lookup.length) { index = 0; } return lookup[index].value; }; /** * See note on SelectionType.decrement() */ SelectionType.prototype.increment = function(value) { var lookup = this.getLookup(); var index = this._findValue(lookup, value); if (index === -1) { // For an increment operation when there is nothing to start from, we // want to start from the top, i.e. index 0, so the value before we // 'increment' (see note above) must be 1. index = 1; } index--; if (index < 0) { index = lookup.length - 1; } return lookup[index].value; }; /** * Walk through an array of { name:.., value:... } objects looking for a * matching value (using strict equality), returning the matched index (or -1 * if not found). * @param lookup Array of objects with name/value properties to search through * @param value The value to search for * @return The index at which the match was found, or -1 if no match was found */ SelectionType.prototype._findValue = function(lookup, value) { var index = -1; for (var i = 0; i < lookup.length; i++) { var pair = lookup[i]; if (pair.value === value) { index = i; break; } } return index; }; SelectionType.prototype.name = 'selection'; exports.SelectionType = SelectionType; }); /* * Copyright (c) 2009 Panagiotis Astithas * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ define('gcli/types/spell', ['require', 'exports', 'module' ], function(require, exports, module) { /** * A spell-checker based on the statistical algorithm described by Peter Norvig * in http://norvig.com/spell-correct.html, and converted to JavaScript by Past * http://past.github.com/speller/ * * Usage requires a two-step process: * 1) call speller.train() one or more times with a large text to train the * language model * 2) call speller.correct(word) to retrieve the correction for the specified * word */ function Speller() { // A map of words to the count of times they were encountered during training. this._nWords = {}; } Speller.letters = "abcdefghijklmnopqrstuvwxyz".split(""); /** * A function that trains the language model with the words in the supplied * text. Multiple invocation of this function can extend the training of the * model. */ Speller.prototype.train = function(words) { words.forEach(function(word) { word = word.toLowerCase(); this._nWords[word] = this._nWords.hasOwnProperty(word) ? this._nWords[word] + 1 : 1; }, this); }; /** * A function that returns the correction for the specified word. */ Speller.prototype.correct = function(word) { if (this._nWords.hasOwnProperty(word)) { return word; } var candidates = {}; var list = this._edits(word); list.forEach(function(edit) { if (this._nWords.hasOwnProperty(edit)) { candidates[this._nWords[edit]] = edit; } }, this); if (this._countKeys(candidates) > 0) { return candidates[this._max(candidates)]; } list.forEach(function(edit) { this._edits(edit).forEach(function(w) { if (this._nWords.hasOwnProperty(w)) { candidates[this._nWords[w]] = w; } }, this); }, this); return this._countKeys(candidates) > 0 ? candidates[this._max(candidates)] : null; }; /** * A helper function that counts the keys in the supplied object. */ Speller.prototype._countKeys = function(object) { // return Object.keys(object).length; ? var count = 0; for (var attr in object) { if (object.hasOwnProperty(attr)) { count++; } } return count; }; /** * A helper function that returns the word with the most occurrences in the * language model, among the supplied candidates. * @param candidates */ Speller.prototype._max = function(candidates) { var arr = []; for (var candidate in candidates) { if (candidates.hasOwnProperty(candidate)) { arr.push(candidate); } } return Math.max.apply(null, arr); }; /** * A function that returns the set of possible corrections of the specified * word. The edits can be deletions, insertions, alterations or transpositions. */ Speller.prototype._edits = function(word) { var results = []; // Deletion for (var i = 0; i < word.length; i++) { results.push(word.slice(0, i) + word.slice(i + 1)); } // Transposition for (i = 0; i < word.length - 1; i++) { results.push(word.slice(0, i) + word.slice(i + 1, i + 2) + word.slice(i, i + 1) + word.slice(i + 2)); } // Alteration for (i = 0; i < word.length; i++) { Speller.letters.forEach(function(l) { results.push(word.slice(0, i) + l + word.slice(i + 1)); }, this); } // Insertion for (i = 0; i <= word.length; i++) { Speller.letters.forEach(function(l) { results.push(word.slice(0, i) + l + word.slice(i)); }, this); } return results; }; exports.Speller = Speller; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/types/command', ['require', 'exports', 'module' , 'gcli/canon', 'gcli/l10n', 'gcli/types', 'gcli/types/selection'], function(require, exports, module) { var canon = require('gcli/canon'); var l10n = require('gcli/l10n'); var types = require('gcli/types'); var SelectionType = require('gcli/types/selection').SelectionType; var Status = require('gcli/types').Status; var Conversion = require('gcli/types').Conversion; /** * Registration and de-registration. */ exports.startup = function() { types.registerType(CommandType); types.registerType(ParamType); }; exports.shutdown = function() { types.unregisterType(CommandType); types.unregisterType(ParamType); }; /** * 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 ParamType(typeSpec) { this.requisition = typeSpec.requisition; this.isIncompleteName = typeSpec.isIncompleteName; this.stringifyProperty = 'name'; } ParamType.prototype = Object.create(SelectionType.prototype); ParamType.prototype.name = 'param'; ParamType.prototype.lookup = function() { var displayedParams = []; var command = this.requisition.commandAssignment.value; command.params.forEach(function(param) { var arg = this.requisition.getAssignment(param.name).arg; if (!param.isPositionalAllowed && arg.type === "BlankArgument") { displayedParams.push({ name: '--' + param.name, value: param }); } }, this); return displayedParams; }; ParamType.prototype.parse = function(arg) { return this.isIncompleteName ? SelectionType.prototype.parse.call(this, arg) : new Conversion(undefined, arg, Status.ERROR, l10n.lookup('cliUnusedArg')); }; /** * Select from the available commands. * This is very similar to a SelectionType, however the level of hackery in * SelectionType to make it handle Commands correctly was to high, so we * simplified. * If you are making changes to this code, you should check there too. */ function CommandType() { this.stringifyProperty = 'name'; } CommandType.prototype = Object.create(SelectionType.prototype); CommandType.prototype.name = 'command'; CommandType.prototype.lookup = function() { var commands = canon.getCommands(); commands.sort(function(c1, c2) { return c1.name.localeCompare(c2.name); }); return commands.map(function(command) { return { name: command.name, value: command }; }, this); }; /** * Add an option to our list of predicted options */ CommandType.prototype._addToPredictions = function(predictions, option, arg) { if (option.value.hidden) { return; } // The command type needs to exclude sub-commands when the CLI // is blank, but include them when we're filtering. This hack // excludes matches when the filter text is '' and when the // name includes a space. if (arg.text.length !== 0 || option.name.indexOf(' ') === -1) { predictions.push(option); } }; CommandType.prototype.parse = function(arg) { // Especially at startup, predictions live over the time that things change // so we provide a completion function rather than completion values var predictFunc = function() { return this._findPredictions(arg); }.bind(this); var predictions = this._findPredictions(arg); if (predictions.length === 0) { var msg = l10n.lookupFormat('typesSelectionNomatch', [ arg.text ]); return new Conversion(undefined, arg, Status.ERROR, msg, predictFunc); } var command = predictions[0].value; if (predictions.length === 1) { // Is it an exact match of an executable command, // or just the only possibility? if (command.name === arg.text && typeof command.exec === 'function') { return new Conversion(command, arg, Status.VALID, ''); } return new Conversion(undefined, arg, Status.INCOMPLETE, '', predictFunc); } // It's valid if the text matches, even if there are several options if (predictions[0].name === arg.text) { return new Conversion(command, arg, Status.VALID, '', predictFunc); } return new Conversion(undefined, arg, Status.INCOMPLETE, '', predictFunc); }; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/canon', ['require', 'exports', 'module' , 'gcli/util', 'gcli/l10n', 'gcli/types', 'gcli/types/basic', 'gcli/types/selection'], function(require, exports, module) { var canon = exports; var util = require('gcli/util'); var l10n = require('gcli/l10n'); var types = require('gcli/types'); var Status = require('gcli/types').Status; var BooleanType = require('gcli/types/basic').BooleanType; var SelectionType = require('gcli/types/selection').SelectionType; /** * Implement the localization algorithm for any documentation objects (i.e. * description and manual) in a command. * @param data The data assigned to a description or manual property * @param onUndefined If data == null, should we return the data untouched or * lookup a 'we don't know' key in it's place. */ function lookup(data, onUndefined) { if (data == null) { if (onUndefined) { return l10n.lookup(onUndefined); } return data; } if (typeof data === 'string') { return data; } if (typeof data === 'object') { if (data.key) { return l10n.lookup(data.key); } var locales = l10n.getPreferredLocales(); var translated; locales.some(function(locale) { translated = data[locale]; return translated != null; }); if (translated != null) { return translated; } console.error('Can\'t find locale in descriptions: ' + 'locales=' + JSON.stringify(locales) + ', ' + 'description=' + JSON.stringify(data)); return '(No description)'; } return l10n.lookup(onUndefined); } /** * The command object is mostly just setup around a commandSpec (as passed to * #addCommand()). */ function Command(commandSpec) { Object.keys(commandSpec).forEach(function(key) { this[key] = commandSpec[key]; }, this); if (!this.name) { throw new Error('All registered commands must have a name'); } if (this.params == null) { this.params = []; } if (!Array.isArray(this.params)) { throw new Error('command.params must be an array in ' + this.name); } this.description = 'description' in this ? this.description : undefined; this.description = lookup(this.description, 'canonDescNone'); this.manual = 'manual' in this ? this.manual : undefined; this.manual = lookup(this.manual); // At this point this.params has nested param groups. We want to flatten it // out and replace the param object literals with Parameter objects var paramSpecs = this.params; this.params = []; // Track if the user is trying to mix default params and param groups. // All the non-grouped parameters must come before all the param groups // because non-grouped parameters can be assigned positionally, so their // index is important. We don't want 'holes' in the order caused by // parameter groups. var usingGroups = false; if (this.returnType == null) { this.returnType = 'string'; } // In theory this could easily be made recursive, so param groups could // contain nested param groups. Current thinking is that the added // complexity for the UI probably isn't worth it, so this implementation // prevents nesting. paramSpecs.forEach(function(spec) { if (!spec.group) { if (usingGroups) { console.error('Parameters can\'t come after param groups.' + ' Ignoring ' + this.name + '/' + spec.name); } else { var param = new Parameter(spec, this, null); this.params.push(param); } } else { spec.params.forEach(function(ispec) { var param = new Parameter(ispec, this, spec.group); this.params.push(param); }, this); usingGroups = true; } }, this); } canon.Command = Command; /** * A wrapper for a paramSpec so we can sort out shortened versions names for * option switches */ function Parameter(paramSpec, command, groupName) { this.command = command || { name: 'unnamed' }; this.paramSpec = paramSpec; this.name = this.paramSpec.name; this.type = this.paramSpec.type; this.groupName = groupName; this.defaultValue = this.paramSpec.defaultValue; if (!this.name) { throw new Error('In ' + this.command.name + ': all params must have a name'); } var typeSpec = this.type; this.type = types.getType(typeSpec); if (this.type == null) { console.error('Known types: ' + types.getTypeNames().join(', ')); throw new Error('In ' + this.command.name + '/' + this.name + ': can\'t find type for: ' + JSON.stringify(typeSpec)); } // boolean parameters have an implicit defaultValue:false, which should // not be changed. See the docs. if (this.type instanceof BooleanType) { if (this.defaultValue !== undefined) { console.error('In ' + this.command.name + '/' + this.name + ': boolean parameters can not have a defaultValue.' + ' Ignoring'); } this.defaultValue = false; } // Check the defaultValue for validity. // Both undefined and null get a pass on this test. undefined is used when // there is no defaultValue, and null is used when the parameter is // optional, neither are required to parse and stringify. if (this.defaultValue != null) { try { var defaultText = this.type.stringify(this.defaultValue); var defaultConversion = this.type.parseString(defaultText); if (defaultConversion.getStatus() !== Status.VALID) { console.error('In ' + this.command.name + '/' + this.name + ': Error round tripping defaultValue. status = ' + defaultConversion.getStatus()); } } catch (ex) { console.error('In ' + this.command.name + '/' + this.name + ': ' + ex); } } // Some types (boolean, array) have a non 'undefined' blank value. Give the // type a chance to override the default defaultValue of undefined if (this.defaultValue === undefined) { this.defaultValue = this.type.getBlank().value; } // All parameters that can only be set via a named parameter must have a // non-undefined default value if (!this.isPositionalAllowed && this.defaultValue === undefined) { console.error('In ' + this.command.name + '/' + this.name + ': Missing defaultValue for optional parameter.'); } } /** * Does the given name uniquely identify this param (among the other params * in this command) * @param name The name to check */ Parameter.prototype.isKnownAs = function(name) { if (name === '--' + this.name) { return true; } return false; }; /** * Read the default value for this parameter either from the parameter itself * (if this function has been over-ridden) or from the type, or from calling * parseString on an empty string */ Parameter.prototype.getBlank = function() { if (this.type.getBlank) { return this.type.getBlank(); } return this.type.parseString(''); }; /** * Resolve the manual for this parameter, by looking in the paramSpec * and doing a l10n lookup */ Object.defineProperty(Parameter.prototype, 'manual', { get: function() { return lookup(this.paramSpec.manual || undefined); }, enumerable: true }); /** * Resolve the description for this parameter, by looking in the paramSpec * and doing a l10n lookup */ Object.defineProperty(Parameter.prototype, 'description', { get: function() { return lookup(this.paramSpec.description || undefined, 'canonDescNone'); }, enumerable: true }); /** * Is the user required to enter data for this parameter? (i.e. has * defaultValue been set to something other than undefined) */ Object.defineProperty(Parameter.prototype, 'isDataRequired', { get: function() { return this.defaultValue === undefined; }, enumerable: true }); /** * Are we allowed to assign data to this parameter using positional * parameters? */ Object.defineProperty(Parameter.prototype, 'isPositionalAllowed', { get: function() { return this.groupName == null; }, enumerable: true }); canon.Parameter = Parameter; /** * A lookup hash of our registered commands */ var commands = {}; /** * A sorted list of command names, we regularly want them in order, so pre-sort */ var commandNames = []; /** * A lookup of the original commandSpecs by command name */ var commandSpecs = {}; /** * Add a command to the canon of known commands. * This function is exposed to the outside world (via gcli/index). It is * documented in docs/index.md for all the world to see. * @param commandSpec The command and its metadata. * @return The new command */ canon.addCommand = function addCommand(commandSpec) { if (commands[commandSpec.name] != null) { // Roughly canon.removeCommand() without the event call, which we do later delete commands[commandSpec.name]; commandNames = commandNames.filter(function(test) { return test !== commandSpec.name; }); } var command = new Command(commandSpec); commands[commandSpec.name] = command; commandNames.push(commandSpec.name); commandNames.sort(); commandSpecs[commandSpec.name] = commandSpec; canon.onCanonChange(); return command; }; /** * Remove an individual command. The opposite of #addCommand(). * Removing a non-existent command is a no-op. * @param commandOrName Either a command name or the command itself. * @return true if a command was removed, false otherwise. */ canon.removeCommand = function removeCommand(commandOrName) { var name = typeof commandOrName === 'string' ? commandOrName : commandOrName.name; if (!commands[name]) { return false; } // See start of canon.addCommand if changing this code delete commands[name]; delete commandSpecs[name]; commandNames = commandNames.filter(function(test) { return test !== name; }); canon.onCanonChange(); return true; }; /** * Retrieve a command by name * @param name The name of the command to retrieve */ canon.getCommand = function getCommand(name) { // '|| undefined' is to silence 'reference to undefined property' warnings return commands[name] || undefined; }; /** * Get an array of all the registered commands. */ canon.getCommands = function getCommands() { // return Object.values(commands); return Object.keys(commands).map(function(name) { return commands[name]; }, this); }; /** * Get an array containing the names of the registered commands. */ canon.getCommandNames = function getCommandNames() { return commandNames.slice(0); }; /** * Get access to the stored commandMetaDatas (i.e. before they were made into * instances of Command/Parameters) so we can remote them. */ canon.getCommandSpecs = function getCommandSpecs() { return commandSpecs; }; /** * Enable people to be notified of changes to the list of commands */ canon.onCanonChange = util.createEvent('canon.onCanonChange'); /** * CommandOutputManager stores the output objects generated by executed * commands. * * CommandOutputManager is exposed (via canon.commandOutputManager) to the the * outside world and could (but shouldn't) be used before gcli.startup() has * been called. This could should be defensive to that where possible, and we * should certainly document if the use of it or similar will fail if used too * soon. */ function CommandOutputManager() { this.onOutput = util.createEvent('CommandOutputManager.onOutput'); } canon.CommandOutputManager = CommandOutputManager; /** * We maintain a global command output manager for the majority case where * there is only one important set of outputs. */ canon.commandOutputManager = new CommandOutputManager(); }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/util', ['require', 'exports', 'module' ], function(require, exports, module) { /* * A number of DOM manipulation and event handling utilities. */ //------------------------------------------------------------------------------ var eventDebug = false; /** * Useful way to create a name for a handler, used in createEvent() */ function nameFunction(handler) { var scope = handler.scope ? handler.scope.constructor.name + '.' : ''; var name = handler.func.name; if (name) { return scope + name; } for (var prop in handler.scope) { if (handler.scope[prop] === handler.func) { return scope + prop; } } return scope + handler.func; } /** * Create an event. * For use as follows: * * function Hat() { * this.putOn = createEvent('Hat.putOn'); * ... * } * Hat.prototype.adorn = function(person) { * this.putOn({ hat: hat, person: person }); * ... * } * * var hat = new Hat(); * hat.putOn.add(function(ev) { * console.log('The hat ', ev.hat, ' has is worn by ', ev.person); * }, scope); * * @param name Optional name to help with debugging */ exports.createEvent = function(name) { var handlers = []; var holdFire = false; var heldEvents = []; var eventCombiner = undefined; /** * This is how the event is triggered. * @param ev The event object to be passed to the event listeners */ var event = function(ev) { if (holdFire) { heldEvents.push(ev); if (eventDebug) { console.log('Held fire: ' + name, ev); } return; } if (eventDebug) { console.group('Fire: ' + name + ' to ' + handlers.length + ' listeners', ev); } // Use for rather than forEach because it step debugs better, which is // important for debugging events for (var i = 0; i < handlers.length; i++) { var handler = handlers[i]; if (eventDebug) { console.log(nameFunction(handler)); } handler.func.call(handler.scope, ev); } if (eventDebug) { console.groupEnd(); } }; /** * Add a new handler function * @param func The function to call when this event is triggered * @param scope Optional 'this' object for the function call */ event.add = function(func, scope) { handlers.push({ func: func, scope: scope }); }; /** * Remove a handler function added through add(). Both func and scope must * be strict equals (===) the values used in the call to add() * @param func The function to call when this event is triggered * @param scope Optional 'this' object for the function call */ event.remove = function(func, scope) { var found = false; handlers = handlers.filter(function(test) { var noMatch = (test.func !== func && test.scope !== scope); if (!noMatch) { found = true; } return noMatch; }); if (!found) { console.warn('Failed to remove handler from ' + name); } }; /** * Remove all handlers. * Reset the state of this event back to it's post create state */ event.removeAll = function() { handlers = []; }; /** * Temporarily prevent this event from firing. * @see resumeFire(ev) */ event.holdFire = function() { if (eventDebug) { console.group('Holding fire: ' + name); } holdFire = true; }; /** * Resume firing events. * If there are heldEvents, then we fire one event to cover them all. If an * event combining function has been provided then we use that to combine the * events. Otherwise the last held event is used. * @see holdFire() */ event.resumeFire = function() { if (eventDebug) { console.groupEnd('Resume fire: ' + name); } if (holdFire !== true) { throw new Error('Event not held: ' + name); } holdFire = false; if (heldEvents.length === 0) { return; } if (heldEvents.length === 1) { event(heldEvents[0]); } else { var first = heldEvents[0]; var last = heldEvents[heldEvents.length - 1]; if (eventCombiner) { event(eventCombiner(first, last, heldEvents)); } else { event(last); } } heldEvents = []; }; /** * When resumeFire has a number of events to combine, by default it just * picks the last, however you can provide an eventCombiner which returns a * combined event. * eventCombiners will be passed 3 parameters: * - first The first event to be held * - last The last event to be held * - all An array containing all the held events * The return value from an eventCombiner is expected to be an event object */ Object.defineProperty(event, 'eventCombiner', { set: function(newEventCombiner) { if (typeof newEventCombiner !== 'function') { throw new Error('eventCombiner is not a function'); } eventCombiner = newEventCombiner; }, enumerable: true }); return event; }; //------------------------------------------------------------------------------ /** * XHTML namespace */ exports.NS_XHTML = 'http://www.w3.org/1999/xhtml'; /** * XUL namespace */ exports.NS_XUL = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'; /** * Create an HTML or XHTML element depending on whether the document is HTML * or XML based. Where HTML/XHTML elements are distinguished by whether they * are created using doc.createElementNS('http://www.w3.org/1999/xhtml', tag) * or doc.createElement(tag) * If you want to create a XUL element then you don't have a problem knowing * what namespace you want. * @param doc The document in which to create the element * @param tag The name of the tag to create * @returns The created element */ exports.createElement = function(doc, tag) { if (exports.isXmlDocument(doc)) { return doc.createElementNS(exports.NS_XHTML, tag); } else { return doc.createElement(tag); } }; /** * Remove all the child nodes from this node * @param elem The element that should have it's children removed */ exports.clearElement = function(elem) { while (elem.hasChildNodes()) { elem.removeChild(elem.firstChild); } }; var isAllWhitespace = /^\s*$/; /** * Iterate over the children of a node looking for TextNodes that have only * whitespace content and remove them. * This utility is helpful when you have a template which contains whitespace * so it looks nice, but where the whitespace interferes with the rendering of * the page * @param elem The element which should have blank whitespace trimmed * @param deep Should this node removal include child elements */ exports.removeWhitespace = function(elem, deep) { var i = 0; while (i < elem.childNodes.length) { var child = elem.childNodes.item(i); if (child.nodeType === 3 /*Node.TEXT_NODE*/ && isAllWhitespace.test(child.textContent)) { elem.removeChild(child); } else { if (deep && child.nodeType === 1 /*Node.ELEMENT_NODE*/) { exports.removeWhitespace(child, deep); } i++; } } }; /** * Create a style element in the document head, and add the given CSS text to * it. * @param cssText The CSS declarations to append * @param doc The document element to work from * @param id Optional id to assign to the created style tag. If the id already * exists on the document, we do not add the CSS again. */ exports.importCss = function(cssText, doc, id) { if (!cssText) { return undefined; } doc = doc || document; if (!id) { id = 'hash-' + hash(cssText); } var found = doc.getElementById(id); if (found) { if (found.tagName.toLowerCase() !== 'style') { console.error('Warning: importCss passed id=' + id + ', but that pre-exists (and isn\'t a style tag)'); } return found; } var style = exports.createElement(doc, 'style'); style.id = id; style.appendChild(doc.createTextNode(cssText)); var head = doc.getElementsByTagName('head')[0] || doc.documentElement; head.appendChild(style); return style; }; /** * Simple hash function which happens to match Java's |String.hashCode()| * Done like this because I we don't need crypto-security, but do need speed, * and I don't want to spend a long time working on it. * @see http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/ */ function hash(str) { var hash = 0; if (str.length == 0) { return hash; } for (var i = 0; i < str.length; i++) { var char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return hash; } /** * There are problems with innerHTML on XML documents, so we need to do a dance * using document.createRange().createContextualFragment() when in XML mode */ exports.setContents = function(elem, contents) { if (typeof HTMLElement !== 'undefined' && contents instanceof HTMLElement) { exports.clearElement(elem); elem.appendChild(contents); return; } if (exports.isXmlDocument(elem.ownerDocument)) { try { var ns = elem.ownerDocument.documentElement.namespaceURI; if (!ns) { ns = exports.NS_XHTML; } exports.clearElement(elem); contents = '
' + contents + '
'; var range = elem.ownerDocument.createRange(); var child = range.createContextualFragment(contents).firstChild; while (child.hasChildNodes()) { elem.appendChild(child.firstChild); } } catch (ex) { console.error('Bad XHTML', ex); console.trace(); throw ex; } } else { elem.innerHTML = contents; } }; /** * Load some HTML into the given document and return a DOM element. * This utility assumes that the html has a single root (other than whitespace) */ exports.toDom = function(document, html) { var div = exports.createElement(document, 'div'); exports.setContents(div, html); return div.children[0]; }; /** * How to detect if we're in an XML document. * In a Mozilla we check that document.xmlVersion = null, however in Chrome * we use document.contentType = undefined. * @param doc The document element to work from (defaulted to the global * 'document' if missing */ exports.isXmlDocument = function(doc) { doc = doc || document; // Best test for Firefox if (doc.contentType && doc.contentType != 'text/html') { return true; } // Best test for Chrome if (doc.xmlVersion != null) { return true; } return false; }; /** * Find the position of [element] in [nodeList]. * @returns an index of the match, or -1 if there is no match */ function positionInNodeList(element, nodeList) { for (var i = 0; i < nodeList.length; i++) { if (element === nodeList[i]) { return i; } } return -1; } /** * Find a unique CSS selector for a given element * @returns a string such that ele.ownerDocument.querySelector(reply) === ele * and ele.ownerDocument.querySelectorAll(reply).length === 1 */ exports.findCssSelector = function(ele) { var document = ele.ownerDocument; if (ele.id && document.getElementById(ele.id) === ele) { return '#' + ele.id; } // Inherently unique by tag name var tagName = ele.tagName.toLowerCase(); if (tagName === 'html') { return 'html'; } if (tagName === 'head') { return 'head'; } if (tagName === 'body') { return 'body'; } if (ele.parentNode == null) { console.log('danger: ' + tagName); } // We might be able to find a unique class name var selector, index, matches; if (ele.classList.length > 0) { for (var i = 0; i < ele.classList.length; i++) { // Is this className unique by itself? selector = '.' + ele.classList.item(i); matches = document.querySelectorAll(selector); if (matches.length === 1) { return selector; } // Maybe it's unique with a tag name? selector = tagName + selector; matches = document.querySelectorAll(selector); if (matches.length === 1) { return selector; } // Maybe it's unique using a tag name and nth-child index = positionInNodeList(ele, ele.parentNode.children) + 1; selector = selector + ':nth-child(' + index + ')'; matches = document.querySelectorAll(selector); if (matches.length === 1) { return selector; } } } // So we can be unique w.r.t. our parent, and use recursion index = positionInNodeList(ele, ele.parentNode.children) + 1; selector = exports.findCssSelector(ele.parentNode) + ' > ' + tagName + ':nth-child(' + index + ')'; return selector; }; /** * Work out the path for images. */ exports.createUrlLookup = function(callingModule) { return function imageUrl(path) { try { return require('text!gcli/ui/' + path); } catch (ex) { // Under node/unamd callingModule is provided by node. This code isn't // the right answer but it's enough to pass all the unit tests and get // test coverage information, which is all we actually care about here. if (callingModule.filename) { return callingModule.filename + path; } var filename = callingModule.id.split('/').pop() + '.js'; if (callingModule.uri.substr(-filename.length) !== filename) { console.error('Can\'t work out path from module.uri/module.id'); return path; } if (callingModule.uri) { var end = callingModule.uri.length - filename.length - 1; return callingModule.uri.substr(0, end) + '/' + path; } return filename + '/' + path; } }; }; /** * Helper to find the 'data-command' attribute and call some action on it. * @see |updateCommand()| and |executeCommand()| */ function withCommand(element, action) { var command = element.getAttribute('data-command'); if (!command) { command = element.querySelector('*[data-command]') .getAttribute('data-command'); } if (command) { action(command); } else { console.warn('Missing data-command for ' + util.findCssSelector(element)); } } /** * Update the requisition to contain the text of the clicked element * @param element The clicked element, containing either a data-command * attribute directly or in a nested element, from which we get the command * to be executed. * @param context Either a Requisition or an ExecutionContext or another object * that contains an |update()| function that follows a similar contract. */ exports.updateCommand = function(element, context) { withCommand(element, function(command) { context.update(command); }); }; /** * Execute the text contained in the element that was clicked * @param element The clicked element, containing either a data-command * attribute directly or in a nested element, from which we get the command * to be executed. * @param context Either a Requisition or an ExecutionContext or another object * that contains an |update()| function that follows a similar contract. */ exports.executeCommand = function(element, context) { withCommand(element, function(command) { context.exec({ visible: true, typed: command }); }); }; //------------------------------------------------------------------------------ /** * 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 (typeof 'KeyEvent' === 'undefined') { exports.KeyEvent = this.KeyEvent; } else { exports.KeyEvent = { DOM_VK_CANCEL: 3, DOM_VK_HELP: 6, DOM_VK_BACK_SPACE: 8, DOM_VK_TAB: 9, DOM_VK_CLEAR: 12, DOM_VK_RETURN: 13, DOM_VK_ENTER: 14, DOM_VK_SHIFT: 16, DOM_VK_CONTROL: 17, DOM_VK_ALT: 18, DOM_VK_PAUSE: 19, DOM_VK_CAPS_LOCK: 20, DOM_VK_ESCAPE: 27, DOM_VK_SPACE: 32, DOM_VK_PAGE_UP: 33, DOM_VK_PAGE_DOWN: 34, DOM_VK_END: 35, DOM_VK_HOME: 36, DOM_VK_LEFT: 37, DOM_VK_UP: 38, DOM_VK_RIGHT: 39, DOM_VK_DOWN: 40, DOM_VK_PRINTSCREEN: 44, DOM_VK_INSERT: 45, DOM_VK_DELETE: 46, DOM_VK_0: 48, DOM_VK_1: 49, DOM_VK_2: 50, DOM_VK_3: 51, DOM_VK_4: 52, DOM_VK_5: 53, DOM_VK_6: 54, DOM_VK_7: 55, DOM_VK_8: 56, DOM_VK_9: 57, DOM_VK_SEMICOLON: 59, DOM_VK_EQUALS: 61, DOM_VK_A: 65, DOM_VK_B: 66, DOM_VK_C: 67, DOM_VK_D: 68, DOM_VK_E: 69, DOM_VK_F: 70, DOM_VK_G: 71, DOM_VK_H: 72, DOM_VK_I: 73, DOM_VK_J: 74, DOM_VK_K: 75, DOM_VK_L: 76, DOM_VK_M: 77, DOM_VK_N: 78, DOM_VK_O: 79, DOM_VK_P: 80, DOM_VK_Q: 81, DOM_VK_R: 82, DOM_VK_S: 83, DOM_VK_T: 84, DOM_VK_U: 85, DOM_VK_V: 86, DOM_VK_W: 87, DOM_VK_X: 88, DOM_VK_Y: 89, DOM_VK_Z: 90, DOM_VK_CONTEXT_MENU: 93, DOM_VK_NUMPAD0: 96, DOM_VK_NUMPAD1: 97, DOM_VK_NUMPAD2: 98, DOM_VK_NUMPAD3: 99, DOM_VK_NUMPAD4: 100, DOM_VK_NUMPAD5: 101, DOM_VK_NUMPAD6: 102, DOM_VK_NUMPAD7: 103, DOM_VK_NUMPAD8: 104, DOM_VK_NUMPAD9: 105, DOM_VK_MULTIPLY: 106, DOM_VK_ADD: 107, DOM_VK_SEPARATOR: 108, DOM_VK_SUBTRACT: 109, DOM_VK_DECIMAL: 110, DOM_VK_DIVIDE: 111, DOM_VK_F1: 112, DOM_VK_F2: 113, DOM_VK_F3: 114, DOM_VK_F4: 115, DOM_VK_F5: 116, DOM_VK_F6: 117, DOM_VK_F7: 118, DOM_VK_F8: 119, DOM_VK_F9: 120, DOM_VK_F10: 121, DOM_VK_F11: 122, DOM_VK_F12: 123, DOM_VK_F13: 124, DOM_VK_F14: 125, DOM_VK_F15: 126, DOM_VK_F16: 127, DOM_VK_F17: 128, DOM_VK_F18: 129, DOM_VK_F19: 130, DOM_VK_F20: 131, DOM_VK_F21: 132, DOM_VK_F22: 133, DOM_VK_F23: 134, DOM_VK_F24: 135, DOM_VK_NUM_LOCK: 144, DOM_VK_SCROLL_LOCK: 145, DOM_VK_COMMA: 188, DOM_VK_PERIOD: 190, DOM_VK_SLASH: 191, DOM_VK_BACK_QUOTE: 192, DOM_VK_OPEN_BRACKET: 219, DOM_VK_BACK_SLASH: 220, DOM_VK_CLOSE_BRACKET: 221, DOM_VK_QUOTE: 222, DOM_VK_META: 224 }; } }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/types/javascript', ['require', 'exports', 'module' , 'gcli/l10n', 'gcli/types'], function(require, exports, module) { var l10n = require('gcli/l10n'); var types = require('gcli/types'); var Conversion = types.Conversion; var Type = types.Type; var Status = types.Status; /** * Registration and de-registration. */ exports.startup = function() { types.registerType(JavascriptType); }; exports.shutdown = function() { types.unregisterType(JavascriptType); }; /** * The object against which we complete, which is usually 'window' if it exists * but could be something else in non-web-content environments. */ var globalObject; if (typeof window !== 'undefined') { globalObject = window; } /** * Setter for the object against which JavaScript completions happen */ exports.setGlobalObject = function(obj) { globalObject = obj; }; /** * Getter for the object against which JavaScript completions happen, for use * in testing */ exports.getGlobalObject = function() { return globalObject; }; /** * Remove registration of object against which JavaScript completions happen */ exports.unsetGlobalObject = function() { globalObject = undefined; }; /** * 'javascript' handles scripted input */ function JavascriptType(typeSpec) { } JavascriptType.prototype = Object.create(Type.prototype); JavascriptType.prototype.stringify = function(value) { if (value == null) { return ''; } return value; }; /** * When sorting out completions, there is no point in displaying millions of * matches - this the number of matches that we aim for */ JavascriptType.MAX_COMPLETION_MATCHES = 10; JavascriptType.prototype.parse = function(arg) { var typed = arg.text; var scope = globalObject; // No input is undefined if (typed === '') { return new Conversion(undefined, arg, Status.INCOMPLETE); } // Just accept numbers if (!isNaN(parseFloat(typed)) && isFinite(typed)) { return new Conversion(typed, arg); } // Just accept constants like true/false/null/etc if (typed.trim().match(/(null|undefined|NaN|Infinity|true|false)/)) { return new Conversion(typed, arg); } // Analyze the input text and find the beginning of the last part that // should be completed. var beginning = this._findCompletionBeginning(typed); // There was an error analyzing the string. if (beginning.err) { return new Conversion(typed, arg, Status.ERROR, beginning.err); } // If the current state is ParseState.COMPLEX, then we can't do completion. // so bail out now if (beginning.state === ParseState.COMPLEX) { return new Conversion(typed, arg); } // If the current state is not ParseState.NORMAL, then we are inside of a // string which means that no completion is possible. if (beginning.state !== ParseState.NORMAL) { return new Conversion(typed, arg, Status.INCOMPLETE, ''); } var completionPart = typed.substring(beginning.startPos); var properties = completionPart.split('.'); var matchProp; var prop; if (properties.length > 1) { matchProp = properties.pop().trimLeft(); for (var i = 0; i < properties.length; i++) { prop = properties[i].trim(); // We can't complete on null.foo, so bail out if (scope == null) { return new Conversion(typed, arg, Status.ERROR, l10n.lookup('jstypeParseScope')); } if (prop === '') { return new Conversion(typed, arg, Status.INCOMPLETE, ''); } // Check if prop is a getter function on 'scope'. Functions can change // other stuff so we can't execute them to get the next object. Stop here. if (this._isSafeProperty(scope, prop)) { return new Conversion(typed, arg); } try { scope = scope[prop]; } catch (ex) { // It would be nice to be able to report this error in some way but // as it can happen just when someone types '{sessionStorage.', it // almost doesn't really count as an error, so we ignore it return new Conversion(typed, arg, Status.VALID, ''); } } } else { matchProp = properties[0].trimLeft(); } // If the reason we just stopped adjusting the scope was a non-simple string, // then we're not sure if the input is valid or invalid, so accept it if (prop && !prop.match(/^[0-9A-Za-z]*$/)) { return new Conversion(typed, arg); } // However if the prop was a simple string, it is an error if (scope == null) { return new Conversion(typed, arg, Status.ERROR, l10n.lookupFormat('jstypeParseMissing', [ prop ])); } // If the thing we're looking for isn't a simple string, then we're not going // to find it, but we're not sure if it's valid or invalid, so accept it if (!matchProp.match(/^[0-9A-Za-z]*$/)) { return new Conversion(typed, arg); } // Skip Iterators and Generators. if (this._isIteratorOrGenerator(scope)) { return null; } var matchLen = matchProp.length; var prefix = matchLen === 0 ? typed : typed.slice(0, -matchLen); var status = Status.INCOMPLETE; var message = ''; // We really want an array of matches (for sorting) but it's easier to // detect existing members if we're using a map initially var matches = {}; // We only display a maximum of MAX_COMPLETION_MATCHES, so there is no point // in digging up the prototype chain for matches that we're never going to // use. Initially look for matches directly on the object itself and then // look up the chain to find more var distUpPrototypeChain = 0; var root = scope; try { while (root != null && Object.keys(matches).length < JavascriptType.MAX_COMPLETION_MATCHES) { Object.keys(root).forEach(function(property) { // Only add matching properties. Also, as we're walking up the // prototype chain, properties on 'higher' prototypes don't override // similarly named properties lower down if (property.indexOf(matchProp) === 0 && !(property in matches)) { matches[property] = { prop: property, distUpPrototypeChain: distUpPrototypeChain }; } }); distUpPrototypeChain++; root = Object.getPrototypeOf(root); } } catch (ex) { return new Conversion(typed, arg, Status.INCOMPLETE, ''); } // Convert to an array for sorting, and while we're at it, note if we got // an exact match so we know that this input is valid matches = Object.keys(matches).map(function(property) { if (property === matchProp) { status = Status.VALID; } return matches[property]; }); // The sort keys are: // - Being on the object itself, not in the prototype chain // - The lack of existence of a vendor prefix // - The name matches.sort(function(m1, m2) { if (m1.distUpPrototypeChain !== m2.distUpPrototypeChain) { return m1.distUpPrototypeChain - m2.distUpPrototypeChain; } // Push all vendor prefixes to the bottom of the list return isVendorPrefixed(m1.prop) ? (isVendorPrefixed(m2.prop) ? m1.prop.localeCompare(m2.prop) : 1) : (isVendorPrefixed(m2.prop) ? -1 : m1.prop.localeCompare(m2.prop)); }); // Trim to size. There is a bug for doing a better job of finding matches // (bug 682694), but in the mean time there is a performance problem // associated with creating a large number of DOM nodes that few people will // ever read, so trim ... if (matches.length > JavascriptType.MAX_COMPLETION_MATCHES) { matches = matches.slice(0, JavascriptType.MAX_COMPLETION_MATCHES - 1); } // Decorate the matches with: // - a description // - a value (for the menu) and, // - an incomplete flag which reports if we should assume that the user isn't // going to carry on the JS expression with this input so far var predictions = matches.map(function(match) { var description; var incomplete = true; if (this._isSafeProperty(scope, match.prop)) { description = '(property getter)'; } else { try { var value = scope[match.prop]; if (typeof value === 'function') { description = '(function)'; } else if (typeof value === 'boolean' || typeof value === 'number') { description = '= ' + value; incomplete = false; } else if (typeof value === 'string') { if (value.length > 40) { value = value.substring(0, 37) + '…'; } description = '= \'' + value + '\''; incomplete = false; } else { description = '(' + typeof value + ')'; } } catch (ex) { description = '(' + l10n.lookup('jstypeParseError') + ')'; } } return { name: prefix + match.prop, value: { name: prefix + match.prop, description: description }, description: description, incomplete: incomplete }; }, this); if (predictions.length === 0) { status = Status.ERROR; message = l10n.lookupFormat('jstypeParseMissing', [ matchProp ]); } // If the match is the only one possible, and its VALID, predict nothing if (predictions.length === 1 && status === Status.VALID) { predictions = undefined; } return new Conversion(typed, arg, status, message, predictions); }; /** * Does the given property have a prefix that indicates that it is vendor * specific? */ function isVendorPrefixed(name) { return name.indexOf('moz') === 0 || name.indexOf('webkit') === 0 || name.indexOf('ms') === 0; } /** * Constants used in return value of _findCompletionBeginning() */ var ParseState = { /** * We have simple input like window.foo, without any punctuation that makes * completion prediction be confusing or wrong */ NORMAL: 0, /** * The cursor is in some Javascript that makes completion hard to predict, * like console.log( */ COMPLEX: 1, /** * The cursor is inside single quotes (') */ QUOTE: 2, /** * The cursor is inside single quotes (") */ DQUOTE: 3 }; var OPEN_BODY = '{[('.split(''); var CLOSE_BODY = '}])'.split(''); var OPEN_CLOSE_BODY = { '{': '}', '[': ']', '(': ')' }; /** * How we distinguish between simple and complex JS input. We attempt * completion against simple JS. */ var simpleChars = /[a-zA-Z0-9.]/; /** * Analyzes a given string to find the last statement that is interesting for * later completion. * @param text A string to analyze * @return If there was an error in the string detected, then a object like * { err: 'ErrorMesssage' } * is returned, otherwise a object like * { * state: ParseState.NORMAL|ParseState.QUOTE|ParseState.DQUOTE, * startPos: index of where the last statement begins * } */ JavascriptType.prototype._findCompletionBeginning = function(text) { var bodyStack = []; var state = ParseState.NORMAL; var start = 0; var c; var complex = false; for (var i = 0; i < text.length; i++) { c = text[i]; if (!simpleChars.test(c)) { complex = true; } switch (state) { // Normal JS state. case ParseState.NORMAL: if (c === '"') { state = ParseState.DQUOTE; } else if (c === '\'') { state = ParseState.QUOTE; } else if (c === ';') { start = i + 1; } else if (c === ' ') { start = i + 1; } else if (OPEN_BODY.indexOf(c) != -1) { bodyStack.push({ token: c, start: start }); start = i + 1; } else if (CLOSE_BODY.indexOf(c) != -1) { var last = bodyStack.pop(); if (!last || OPEN_CLOSE_BODY[last.token] != c) { return { err: l10n.lookup('jstypeBeginSyntax') }; } if (c === '}') { start = i + 1; } else { start = last.start; } } break; // Double quote state > " < case ParseState.DQUOTE: if (c === '\\') { i ++; } else if (c === '\n') { return { err: l10n.lookup('jstypeBeginUnterm') }; } else if (c === '"') { state = ParseState.NORMAL; } break; // Single quote state > ' < case ParseState.QUOTE: if (c === '\\') { i ++; } else if (c === '\n') { return { err: l10n.lookup('jstypeBeginUnterm') }; } else if (c === '\'') { state = ParseState.NORMAL; } break; } } if (state === ParseState.NORMAL && complex) { state = ParseState.COMPLEX; } return { state: state, startPos: start }; }; /** * Return true if the passed object is either an iterator or a generator, and * false otherwise * @param obj The object to check */ JavascriptType.prototype._isIteratorOrGenerator = function(obj) { if (obj === null) { return false; } if (typeof aObject === 'object') { if (typeof obj.__iterator__ === 'function' || obj.constructor && obj.constructor.name === 'Iterator') { return true; } try { var str = obj.toString(); if (typeof obj.next === 'function' && str.indexOf('[object Generator') === 0) { return true; } } catch (ex) { // window.history.next throws in the typeof check above. return false; } } return false; }; /** * Would calling 'scope[prop]' cause the invocation of a non-native (i.e. user * defined) function property? * Since calling functions can have side effects, it's only safe to do that if * explicitly requested, rather than because we're trying things out for the * purposes of completion. */ JavascriptType.prototype._isSafeProperty = function(scope, prop) { if (typeof scope !== 'object') { return false; } // Walk up the prototype chain of 'scope' looking for a property descriptor // for 'prop' var propDesc; while (scope) { try { propDesc = Object.getOwnPropertyDescriptor(scope, prop); if (propDesc) { break; } } catch (ex) { // Native getters throw here. See bug 520882. if (ex.name === 'NS_ERROR_XPC_BAD_CONVERT_JS' || ex.name === 'NS_ERROR_XPC_BAD_OP_ON_WN_PROTO') { return false; } return true; } scope = Object.getPrototypeOf(scope); } if (!propDesc) { return false; } if (!propDesc.get) { return false; } // The property is safe if 'get' isn't a function or if the function has a // prototype (in which case it's native) return typeof propDesc.get !== 'function' || 'prototype' in propDesc.get; }; JavascriptType.prototype.name = 'javascript'; exports.JavascriptType = JavascriptType; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/types/node', ['require', 'exports', 'module' , 'gcli/host', 'gcli/l10n', 'gcli/types'], function(require, exports, module) { var host = require('gcli/host'); var l10n = require('gcli/l10n'); var types = require('gcli/types'); var Type = require('gcli/types').Type; var Status = require('gcli/types').Status; var Conversion = require('gcli/types').Conversion; /** * Registration and de-registration. */ exports.startup = function() { types.registerType(NodeType); }; exports.shutdown = function() { types.unregisterType(NodeType); }; /** * The object against which we complete, which is usually 'window' if it exists * but could be something else in non-web-content environments. */ var doc; if (typeof document !== 'undefined') { doc = document; } /** * Setter for the document that contains the nodes we're matching */ exports.setDocument = function(document) { doc = document; }; /** * Undo the effects of setDocument() */ exports.unsetDocument = function() { doc = undefined; }; /** * Getter for the document that contains the nodes we're matching * Most for changing things back to how they were for unit testing */ exports.getDocument = function() { return doc; }; /** * A CSS expression that refers to a single node */ function NodeType(typeSpec) { } NodeType.prototype = Object.create(Type.prototype); NodeType.prototype.stringify = function(value) { if (value == null) { return ''; } return value.__gcliQuery || 'Error'; }; NodeType.prototype.parse = function(arg) { if (arg.text === '') { return new Conversion(undefined, arg, Status.INCOMPLETE); } var nodes; try { nodes = doc.querySelectorAll(arg.text); } catch (ex) { return new Conversion(undefined, arg, Status.ERROR, l10n.lookup('nodeParseSyntax')); } if (nodes.length === 0) { return new Conversion(undefined, arg, Status.INCOMPLETE, l10n.lookup('nodeParseNone')); } if (nodes.length === 1) { var node = nodes.item(0); node.__gcliQuery = arg.text; host.flashNodes(node, true); return new Conversion(node, arg, Status.VALID, ''); } host.flashNodes(nodes, false); return new Conversion(undefined, arg, Status.ERROR, l10n.lookupFormat('nodeParseMultiple', [ nodes.length ])); }; NodeType.prototype.name = 'node'; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/host', ['require', 'exports', 'module' ], function(require, exports, module) { /** * The chromeWindow as as required by Highlighter, so it knows where to * create temporary highlight nodes. */ exports.chromeWindow = undefined; /** * Helper to turn a set of nodes background another color for 0.5 seconds. * There is likely a better way to do this, but this will do for now. */ exports.flashNodes = function(nodes, match) { // Commented out until Bug 653545 is completed /* if (exports.chromeWindow == null) { console.log('flashNodes has no chromeWindow. Skipping flash'); return; } var imports = {}; Components.utils.import("resource:///modules/highlighter.jsm", imports); imports.Highlighter.flashNodes(nodes, exports.chromeWindow, match); */ }; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/types/resource', ['require', 'exports', 'module' , 'gcli/types', 'gcli/types/selection'], function(require, exports, module) { var types = require('gcli/types'); var SelectionType = require('gcli/types/selection').SelectionType; /** * Registration and de-registration. */ exports.startup = function() { types.registerType(ResourceType); }; exports.shutdown = function() { types.unregisterType(ResourceType); exports.clearResourceCache(); }; exports.clearResourceCache = function() { ResourceCache.clear(); }; /** * The object against which we complete, which is usually 'window' if it exists * but could be something else in non-web-content environments. */ var doc; if (typeof document !== 'undefined') { doc = document; } /** * Setter for the document that contains the nodes we're matching */ exports.setDocument = function(document) { doc = document; }; /** * Undo the effects of setDocument() */ exports.unsetDocument = function() { ResourceCache.clear(); doc = undefined; }; /** * Getter for the document that contains the nodes we're matching * Most for changing things back to how they were for unit testing */ exports.getDocument = function() { return doc; }; /** * Resources are bits of CSS and JavaScript that the page either includes * directly or as a result of reading some remote resource. * Resource should not be used directly, but instead through a sub-class like * CssResource or ScriptResource. */ function Resource(name, type, inline, element) { this.name = name; this.type = type; this.inline = inline; this.element = element; } /** * Get the contents of the given resource as a string. * The base Resource leaves this unimplemented. */ Resource.prototype.getContents = function() { throw new Error('not implemented'); }; Resource.TYPE_SCRIPT = 'text/javascript'; Resource.TYPE_CSS = 'text/css'; /** * A CssResource provides an implementation of Resource that works for both * [style] elements and [link type='text/css'] elements in the [head]. */ function CssResource(domSheet) { this.name = domSheet.href; if (!this.name) { this.name = domSheet.ownerNode.id ? 'css#' + domSheet.ownerNode.id : 'inline-css'; } this.inline = (domSheet.href == null); this.type = Resource.TYPE_CSS; this.element = domSheet; } CssResource.prototype = Object.create(Resource.prototype); CssResource.prototype.loadContents = function(callback) { callback(this.element.ownerNode.innerHTML); }; CssResource._getAllStyles = function() { var resources = []; if (doc == null) { return resources; } Array.prototype.forEach.call(doc.styleSheets, function(domSheet) { CssResource._getStyle(domSheet, resources); }); dedupe(resources, function(clones) { for (var i = 0; i < clones.length; i++) { clones[i].name = clones[i].name + '-' + i; } }); return resources; }; CssResource._getStyle = function(domSheet, resources) { var resource = ResourceCache.get(domSheet); if (!resource) { resource = new CssResource(domSheet); ResourceCache.add(domSheet, resource); } resources.push(resource); // Look for imported stylesheets try { Array.prototype.forEach.call(domSheet.cssRules, function(domRule) { if (domRule.type == CSSRule.IMPORT_RULE && domRule.styleSheet) { CssResource._getStyle(domRule.styleSheet, resources); } }, this); } catch (ex) { // For system stylesheets } }; /** * A ScriptResource provides an implementation of Resource that works for * [script] elements (both with a src attribute, and used directly). */ function ScriptResource(scriptNode) { this.name = scriptNode.src; if (!this.name) { this.name = scriptNode.id ? 'script#' + scriptNode.id : 'inline-script'; } this.inline = (scriptNode.src === '' || scriptNode.src == null); this.type = Resource.TYPE_SCRIPT; this.element = scriptNode; } ScriptResource.prototype = Object.create(Resource.prototype); ScriptResource.prototype.loadContents = function(callback) { if (this.inline) { callback(this.element.innerHTML); } else { // It would be good if there was a better way to get the script source var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState !== xhr.DONE) { return; } callback(xhr.responseText); }; xhr.open('GET', this.element.src, true); xhr.send(); } }; ScriptResource._getAllScripts = function() { if (doc == null) { return []; } var scriptNodes = doc.querySelectorAll('script'); var resources = Array.prototype.map.call(scriptNodes, function(scriptNode) { var resource = ResourceCache.get(scriptNode); if (!resource) { resource = new ScriptResource(scriptNode); ResourceCache.add(scriptNode, resource); } return resource; }); dedupe(resources, function(clones) { for (var i = 0; i < clones.length; i++) { clones[i].name = clones[i].name + '-' + i; } }); return resources; }; /** * Find resources with the same name, and call onDupe to change the names */ function dedupe(resources, onDupe) { // first create a map of name->[array of resources with same name] var names = {}; resources.forEach(function(scriptResource) { if (names[scriptResource.name] == null) { names[scriptResource.name] = []; } names[scriptResource.name].push(scriptResource); }); // Call the de-dupe function for each set of dupes Object.keys(names).forEach(function(name) { var clones = names[name]; if (clones.length > 1) { onDupe(clones); } }); } /** * Use the Resource implementations to create a type based on SelectionType */ function ResourceType(typeSpec) { this.include = typeSpec.include; if (this.include !== Resource.TYPE_SCRIPT && this.include !== Resource.TYPE_CSS && this.include != null) { throw new Error('invalid include property: ' + this.include); } } ResourceType.prototype = Object.create(SelectionType.prototype); /** * There are several ways to get selection data. This unifies them into one * single function. * @return A map of names to values. */ ResourceType.prototype.getLookup = function() { var resources = []; if (this.include !== Resource.TYPE_SCRIPT) { Array.prototype.push.apply(resources, CssResource._getAllStyles()); } if (this.include !== Resource.TYPE_CSS) { Array.prototype.push.apply(resources, ScriptResource._getAllScripts()); } return resources.map(function(resource) { return { name: resource.name, value: resource }; }); }; ResourceType.prototype.name = 'resource'; /** * A quick cache of resources against nodes * TODO: Potential memory leak when the target document has css or script * resources repeatedly added and removed. Solution might be to use a weak * hash map or some such. */ var ResourceCache = { _cached: [], /** * Do we already have a resource that was created for the given node */ get: function(node) { for (var i = 0; i < ResourceCache._cached.length; i++) { if (ResourceCache._cached[i].node === node) { return ResourceCache._cached[i].resource; } } return null; }, /** * Add a resource for a given node */ add: function(node, resource) { ResourceCache._cached.push({ node: node, resource: resource }); }, /** * Drop all cache entries. Helpful to prevent memory leaks */ clear: function() { ResourceCache._cached = []; } }; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/types/setting', ['require', 'exports', 'module' , 'gcli/settings', 'gcli/types', 'gcli/types/selection', 'gcli/types/basic'], function(require, exports, module) { var settings = require('gcli/settings'); var types = require('gcli/types'); var SelectionType = require('gcli/types/selection').SelectionType; var DeferredType = require('gcli/types/basic').DeferredType; /** * Registration and de-registration. */ exports.startup = function() { types.registerType(SettingType); types.registerType(SettingValueType); }; exports.shutdown = function() { types.unregisterType(SettingType); types.unregisterType(SettingValueType); }; /** * This is a whole new level of nasty. 'setting' and 'settingValue' are a pair * for obvious reasons. settingValue is a deferred type - it defers to the type * of the setting, but how do we implement the defer function - how does it * work out its paired setting? * In another parallel universe we pass the requisition to all the parse * methods so we can extract the args in SettingValueType.parse, however that * seems like a lot of churn for a simple way to connect 2 things. So we're * hacking. SettingType tries to keep 'lastSetting' up to date. */ var lastSetting = null; /** * A type for selecting a known setting */ function SettingType(typeSpec) { settings.onChange.add(function(ev) { this.clearCache(); }, this); } SettingType.prototype = new SelectionType({ cacheable: true }); SettingType.prototype.lookup = function() { return settings.getAll().map(function(setting) { return { name: setting.name, value: setting }; }); }; SettingType.prototype.noMatch = function() { lastSetting = null; }; SettingType.prototype.stringify = function(option) { lastSetting = option; return SelectionType.prototype.stringify.call(this, option); }; SettingType.prototype.parse = function(arg) { var conversion = SelectionType.prototype.parse.call(this, arg); lastSetting = conversion.value; return conversion; }; SettingType.prototype.name = 'setting'; /** * A type for entering the value of a known setting */ function SettingValueType(typeSpec) { } SettingValueType.prototype = Object.create(DeferredType.prototype); SettingValueType.prototype.defer = function() { if (lastSetting != null) { return lastSetting.type; } else { return types.getType('blank'); } }; SettingValueType.prototype.name = 'settingValue'; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/settings', ['require', 'exports', 'module' , 'gcli/util', 'gcli/types'], function(require, exports, module) { var imports = {}; Components.utils.import('resource://gre/modules/XPCOMUtils.jsm', imports); imports.XPCOMUtils.defineLazyGetter(imports, 'prefBranch', function() { var prefService = Components.classes['@mozilla.org/preferences-service;1'] .getService(Components.interfaces.nsIPrefService); return prefService.getBranch(null) .QueryInterface(Components.interfaces.nsIPrefBranch2); }); imports.XPCOMUtils.defineLazyGetter(imports, 'supportsString', function() { return Components.classes["@mozilla.org/supports-string;1"] .createInstance(Components.interfaces.nsISupportsString); }); var util = require('gcli/util'); var types = require('gcli/types'); var allSettings = []; /** * Cache existing settings on startup */ exports.startup = function() { imports.prefBranch.getChildList('').forEach(function(name) { allSettings.push(new Setting(name)); }.bind(this)); allSettings.sort(function(s1, s2) { return s1.name.localeCompare(s2.name); }.bind(this)); }; exports.shutdown = function() { allSettings = []; }; /** * */ var DEVTOOLS_PREFIX = 'devtools.gcli.'; /** * A class to wrap up the properties of a preference. * @see toolkit/components/viewconfig/content/config.js */ function Setting(prefSpec) { if (typeof prefSpec === 'string') { // We're coming from getAll() i.e. a full listing of prefs this.name = prefSpec; this.description = ''; } else { // A specific addition by GCLI this.name = DEVTOOLS_PREFIX + prefSpec.name; if (prefSpec.ignoreTypeDifference !== true && prefSpec.type) { if (this.type.name !== prefSpec.type) { throw new Error('Locally declared type (' + prefSpec.type + ') != ' + 'Mozilla declared type (' + this.type.name + ') for ' + this.name); } } this.description = prefSpec.description; } this.onChange = util.createEvent('Setting.onChange'); } /** * What type is this property: boolean/integer/string? */ Object.defineProperty(Setting.prototype, 'type', { get: function() { switch (imports.prefBranch.getPrefType(this.name)) { case imports.prefBranch.PREF_BOOL: return types.getType('boolean'); case imports.prefBranch.PREF_INT: return types.getType('number'); case imports.prefBranch.PREF_STRING: return types.getType('string'); default: throw new Error('Unknown type for ' + this.name); } }, enumerable: true }); /** * What type is this property: boolean/integer/string? */ Object.defineProperty(Setting.prototype, 'value', { get: function() { switch (imports.prefBranch.getPrefType(this.name)) { case imports.prefBranch.PREF_BOOL: return imports.prefBranch.getBoolPref(this.name); case imports.prefBranch.PREF_INT: return imports.prefBranch.getIntPref(this.name); case imports.prefBranch.PREF_STRING: var value = imports.prefBranch.getComplexValue(this.name, Components.interfaces.nsISupportsString).data; // In case of a localized string if (/^chrome:\/\/.+\/locale\/.+\.properties/.test(value)) { value = imports.prefBranch.getComplexValue(this.name, Components.interfaces.nsIPrefLocalizedString).data; } return value; default: throw new Error('Invalid value for ' + this.name); } }, set: function(value) { if (imports.prefBranch.prefIsLocked(this.name)) { throw new Error('Locked preference ' + this.name); } switch (imports.prefBranch.getPrefType(this.name)) { case imports.prefBranch.PREF_BOOL: imports.prefBranch.setBoolPref(this.name, value); break; case imports.prefBranch.PREF_INT: imports.prefBranch.setIntPref(this.name, value); break; case imports.prefBranch.PREF_STRING: imports.supportsString.data = value; imports.prefBranch.setComplexValue(this.name, Components.interfaces.nsISupportsString, imports.supportsString); break; default: throw new Error('Invalid value for ' + this.name); } Services.prefs.savePrefFile(null); }, enumerable: true }); /** * Reset this setting to it's initial default value */ Setting.prototype.setDefault = function() { imports.prefBranch.clearUserPref(this.name); Services.prefs.savePrefFile(null); }; /** * 'static' function to get an array containing all known Settings */ exports.getAll = function(filter) { if (filter == null) { return allSettings; } return allSettings.filter(function(setting) { return setting.name.indexOf(filter) !== -1; }); }; /** * Add a new setting. */ exports.addSetting = function(prefSpec) { var setting = new Setting(prefSpec); for (var i = 0; i < allSettings.length; i++) { if (allSettings[i].name === setting.name) { allSettings[i] = setting; } } exports.onChange({ added: setting.name }); return setting; }; /** * Getter for an existing setting. Generally use of this function should be * avoided. Systems that define a setting should export it if they wish it to * be available to the outside, or not otherwise. Use of this function breaks * that boundary and also hides dependencies. Acceptable uses include testing * and embedded uses of GCLI that pre-define all settings (e.g. Firefox) * @param name The name of the setting to fetch * @return The found Setting object, or undefined if the setting was not found */ exports.getSetting = function(name) { var found = undefined; allSettings.some(function(setting) { if (setting.name === name) { found = setting; return true; } return false; }); return found; }; /** * Event for use to detect when the list of settings changes */ exports.onChange = util.createEvent('Settings.onChange'); /** * Remove a setting. A no-op in this case */ exports.removeSetting = function(nameOrSpec) { }; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/ui/intro', ['require', 'exports', 'module' , 'gcli/settings', 'gcli/l10n', 'gcli/util', 'gcli/ui/view', 'gcli/cli', 'text!gcli/ui/intro.html'], function(require, exports, module) { var settings = require('gcli/settings'); var l10n = require('gcli/l10n'); var util = require('gcli/util'); var view = require('gcli/ui/view'); var Output = require('gcli/cli').Output; /** * Record if the user has clicked on 'Got It!' */ var hideIntroSettingSpec = { name: 'hideIntro', type: 'boolean', description: l10n.lookup('hideIntroDesc'), defaultValue: false }; var hideIntro; /** * Register (and unregister) the hide-intro setting */ exports.startup = function() { hideIntro = settings.addSetting(hideIntroSettingSpec); }; exports.shutdown = function() { settings.removeSetting(hideIntroSettingSpec); hideIntro = undefined; }; /** * Called when the UI is ready to add a welcome message to the output */ exports.maybeShowIntro = function(commandOutputManager, context) { if (hideIntro.value) { return; } var output = new Output(); commandOutputManager.onOutput({ output: output }); var viewData = this.createView(context, output); output.complete(viewData); }; /** * Called when the UI is ready to add a welcome message to the output */ exports.createView = function(context, output) { return view.createView({ html: require('text!gcli/ui/intro.html'), options: { stack: 'intro.html' }, data: { l10n: l10n.propertyLookup, onclick: function(ev) { util.updateCommand(ev.currentTarget, context); }, ondblclick: function(ev) { util.executeCommand(ev.currentTarget, context); }, showHideButton: (output != null), onGotIt: function(ev) { hideIntro.value = true; output.onClose(); } } }); }; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/ui/view', ['require', 'exports', 'module' , 'gcli/util', 'gcli/ui/domtemplate'], function(require, exports, module) { var util = require('gcli/util'); var domtemplate = require('gcli/ui/domtemplate'); /** * We want to avoid commands having to create DOM structures because that's * messy and because we're going to need to have command output displayed in * different documents. A View is a way to wrap an HTML template (for * domtemplate) in with the data and options to render the template, so anyone * can later run the template in the context of any document. * View also cuts out a chunk of boiler place code. * @param options The information needed to create the DOM from HTML. Includes: * - html (required): The HTML source, probably from a call to require * - options (default={}): The domtemplate options. See domtemplate for details * - data (default={}): The data to domtemplate. See domtemplate for details. * - css (default=none): Some CSS to be added to the final document. If 'css' * is used, use of cssId is strongly recommended. * - cssId (default=none): An ID to prevent multiple CSS additions. See * util.importCss for more details. * @return An object containing a single function 'appendTo()' which runs the * template adding the result to the specified element. Takes 2 parameters: * - element (required): the element to add to * - clear (default=false): if clear===true then remove all pre-existing * children of 'element' before appending the results of this template. */ exports.createView = function(options) { if (options.html == null) { throw new Error('options.html is missing'); } return { /** * RTTI. Yeah. */ isView: true, /** * Run the template against the document to which element belongs. * @param element The element to append the result to * @param clear Set clear===true to remove all children of element */ appendTo: function(element, clear) { // Strict check on the off-chance that we later think of other options // and want to replace 'clear' with an 'options' parameter, but want to // support backwards compat. if (clear === true) { util.clearElement(element); } element.appendChild(this.toDom(element.ownerDocument)); }, /** * Actually convert the view data into a DOM suitable to be appended to * an element * @param document to use in realizing the template */ toDom: function(document) { if (options.css) { util.importCss(options.css, document, options.cssId); } var child = util.toDom(document, options.html); domtemplate.template(child, options.data || {}, options.options || {}); return child; } }; }; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/ui/domtemplate', ['require', 'exports', 'module' ], function(require, exports, module) { var obj = {}; Components.utils.import('resource:///modules/devtools/Templater.jsm', obj); exports.template = obj.template; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/cli', ['require', 'exports', 'module' , 'gcli/util', 'gcli/ui/view', 'gcli/l10n', 'gcli/canon', 'gcli/promise', 'gcli/types', 'gcli/types/basic', 'gcli/argument'], function(require, exports, module) { var util = require('gcli/util'); var view = require('gcli/ui/view'); var l10n = require('gcli/l10n'); var canon = require('gcli/canon'); var Promise = require('gcli/promise').Promise; var Status = require('gcli/types').Status; var Conversion = require('gcli/types').Conversion; var ArrayType = require('gcli/types/basic').ArrayType; var StringType = require('gcli/types/basic').StringType; var BooleanType = require('gcli/types/basic').BooleanType; var NumberType = require('gcli/types/basic').NumberType; var Argument = require('gcli/argument').Argument; var ArrayArgument = require('gcli/argument').ArrayArgument; var NamedArgument = require('gcli/argument').NamedArgument; var TrueNamedArgument = require('gcli/argument').TrueNamedArgument; var MergedArgument = require('gcli/argument').MergedArgument; var ScriptArgument = require('gcli/argument').ScriptArgument; var evalCommand; /** * Registration and de-registration. */ exports.startup = function() { evalCommand = canon.addCommand(evalCommandSpec); }; exports.shutdown = function() { canon.removeCommand(evalCommandSpec.name); evalCommand = undefined; }; /** * Assignment is a link between a parameter and the data for that parameter. * The data for the parameter is available as in the preferred type and as * an Argument for the CLI. *

We also record validity information where applicable. *

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

Events

* Assignment publishes the following event: