Bug 979030 - Fix srcdir loader breakage after GCLI split up; r=mratcliffe

--HG--
rename : toolkit/devtools/gcli/source/mozilla/gcli/mozui/completer.js => toolkit/devtools/gcli/source/lib/gcli/mozui/completer.js
rename : toolkit/devtools/gcli/source/mozilla/gcli/mozui/ffdisplay.js => toolkit/devtools/gcli/source/lib/gcli/mozui/ffdisplay.js
rename : toolkit/devtools/gcli/source/mozilla/gcli/mozui/inputter.js => toolkit/devtools/gcli/source/lib/gcli/mozui/inputter.js
rename : toolkit/devtools/gcli/source/mozilla/gcli/mozui/tooltip.js => toolkit/devtools/gcli/source/lib/gcli/mozui/tooltip.js
This commit is contained in:
Joe Walker 2014-03-15 08:06:34 +00:00
parent aa951ea934
commit feab6bb9b6
37 changed files with 2685 additions and 4082 deletions

View File

@ -119,7 +119,7 @@ SrcdirProvider.prototype = {
let clientURI = this.fileURI(OS.Path.join(toolkitDir, "client"));
let prettyFastURI = this.fileURI(OS.Path.join(toolkitDir), "pretty-fast.js");
let asyncUtilsURI = this.fileURI(OS.Path.join(toolkitDir), "async-utils.js");
let gcliURI = this.fileURI(OS.Path.join(toolkitDir, "gcli"));
let gcliURI = this.fileURI(OS.Path.join(toolkitDir, "gcli", "source", "lib", "gcli"));
let acornURI = this.fileURI(OS.Path.join(toolkitDir, "acorn"));
let acornWalkURI = OS.Path.join(acornURI, "walk.js");
this.loader = new loader.Loader({

View File

@ -22,7 +22,7 @@ gcli_languages_FILES = $(wildcard $(srcdir)/source/lib/gcli/languages/*)
gcli_languages_DEST = $(FINAL_TARGET)/modules/devtools/gcli/languages
INSTALL_TARGETS += gcli_languages
gcli_mozui_FILES = $(wildcard $(srcdir)/source/mozilla/gcli/mozui/*)
gcli_mozui_FILES = $(wildcard $(srcdir)/source/lib/gcli/mozui/*)
gcli_mozui_DEST = $(FINAL_TARGET)/modules/devtools/gcli/mozui
INSTALL_TARGETS += gcli_mozui
@ -30,28 +30,19 @@ gcli_types_FILES = $(wildcard $(srcdir)/source/lib/gcli/types/*)
gcli_types_DEST = $(FINAL_TARGET)/modules/devtools/gcli/types
INSTALL_TARGETS += gcli_types
gcli_ui_FILES = $(wildcard $(srcdir)/source/lib/gcli/ui/*)
gcli_ui_DEST = $(FINAL_TARGET)/modules/devtools/gcli/ui
INSTALL_TARGETS += gcli_ui
gcli_util_FILES = $(wildcard $(srcdir)/source/lib/gcli/util/*)
gcli_util_DEST = $(FINAL_TARGET)/modules/devtools/gcli/util
INSTALL_TARGETS += gcli_util
gcli_root_FILES = $(wildcard $(srcdir)/source/lib/gcli/*)
gcli_root_DEST = $(FINAL_TARGET)/modules/devtools/gcli
INSTALL_TARGETS += gcli_root
include $(topsrcdir)/config/rules.mk
libs::
$(INSTALL) $(IFLAGS1) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools
$(INSTALL) $(IFLAGS1) $(srcdir)/source/mozilla/gcli/util/domtemplate.js $(FINAL_TARGET)/modules/devtools/gcli/util
$(INSTALL) $(IFLAGS1) $(srcdir)/source/lib/gcli/util/fileparser.js $(FINAL_TARGET)/modules/devtools/gcli/util
$(INSTALL) $(IFLAGS1) $(srcdir)/source/mozilla/gcli/util/filesystem.js $(FINAL_TARGET)/modules/devtools/gcli/util
$(INSTALL) $(IFLAGS1) $(srcdir)/source/mozilla/gcli/util/host.js $(FINAL_TARGET)/modules/devtools/gcli/util
$(INSTALL) $(IFLAGS1) $(srcdir)/source/mozilla/gcli/util/l10n.js $(FINAL_TARGET)/modules/devtools/gcli/util
$(INSTALL) $(IFLAGS1) $(srcdir)/source/lib/gcli/util/legacy.js $(FINAL_TARGET)/modules/devtools/gcli/util
$(INSTALL) $(IFLAGS1) $(srcdir)/source/lib/gcli/util/prism.js $(FINAL_TARGET)/modules/devtools/gcli/util
$(INSTALL) $(IFLAGS1) $(srcdir)/source/mozilla/gcli/util/promise.js $(FINAL_TARGET)/modules/devtools/gcli/util
$(INSTALL) $(IFLAGS1) $(srcdir)/source/lib/gcli/util/spell.js $(FINAL_TARGET)/modules/devtools/gcli/util
$(INSTALL) $(IFLAGS1) $(srcdir)/source/lib/gcli/util/util.js $(FINAL_TARGET)/modules/devtools/gcli/util
$(INSTALL) $(IFLAGS1) $(srcdir)/source/lib/gcli/ui/focus.js $(FINAL_TARGET)/modules/devtools/gcli/ui
$(INSTALL) $(IFLAGS1) $(srcdir)/source/lib/gcli/ui/history.js $(FINAL_TARGET)/modules/devtools/gcli/ui
$(INSTALL) $(IFLAGS1) $(srcdir)/source/lib/gcli/ui/intro.js $(FINAL_TARGET)/modules/devtools/gcli/ui
$(INSTALL) $(IFLAGS1) $(srcdir)/source/lib/gcli/ui/menu.js $(FINAL_TARGET)/modules/devtools/gcli/ui
$(INSTALL) $(IFLAGS1) $(srcdir)/source/lib/gcli/ui/menu.css $(FINAL_TARGET)/modules/devtools/gcli/ui
$(INSTALL) $(IFLAGS1) $(srcdir)/source/lib/gcli/ui/view.js $(FINAL_TARGET)/modules/devtools/gcli/ui
$(INSTALL) $(IFLAGS1) $(srcdir)/source/mozilla/gcli/ui/menu.html $(FINAL_TARGET)/modules/devtools/gcli/ui
$(INSTALL) $(IFLAGS1) $(srcdir)/source/lib/gcli/api.js $(FINAL_TARGET)/modules/devtools/gcli
$(INSTALL) $(IFLAGS1) $(srcdir)/source/lib/gcli/cli.js $(FINAL_TARGET)/modules/devtools/gcli
$(INSTALL) $(IFLAGS1) $(srcdir)/source/mozilla/gcli/index.js $(FINAL_TARGET)/modules/devtools/gcli
$(INSTALL) $(IFLAGS1) $(srcdir)/source/mozilla/gcli/settings.js $(FINAL_TARGET)/modules/devtools/gcli

View File

@ -0,0 +1,102 @@
# The Design of GCLI
## Design Goals
GCLI should be:
- primarily for technical users.
- as fast as a traditional CLI. It should be possible to put your head down,
and look at the keyboard and use GCLI 'blind' at full speed without making
mistakes.
- principled about the way it encourages people to build commands. There is
benefit from unifying the underlying concepts.
- automatically helpful.
GCLI should not attempt to:
- convert existing GUI users to a CLI.
- use natural language input. The closest we should get to natural language is
thinking of commands as ```verb noun --adjective```.
- gain a touch based interface. Whilst it's possible (even probable) that touch
can provide further benefits to command line users, that can wait while we
catch up with 1985.
- slavishly follow the syntax of existing commands, predictability is more
important.
- be a programming language. Shell scripts are mini programming languages but
we have JavaScript sat just next door. It's better to integrate than compete.
## Design Challenges
What has changed since 1970 that might cause us to make changes to the design
of the command line?
### Connection limitations
Unix pre-dates the Internet and treats almost everything as a file. Since the
Internet it could be more useful to use URIs as ways to identify sources of data.
### Memory limitations
Modern computers have something like 6 orders of magnitude more memory than the
PDP-7 on which Unix was developed. Innovations like stdin/stdout and pipes are
ways to connect systems without long-term storage of the results. The ability
to store results for some time (potentially in more than one format)
significantly reduces the need for these concepts. We should make the results
of past commands addressable for re-use at a later time.
There are a number of possible policies for eviction of items from the history.
We should investigate options other than a simple stack.
### Multi-tasking limitations
Multi-tasking was a problem in 1970; the problem was getting a computer to do
many jobs on 1 core. Today the problem is getting a computer to do one job on
many cores. However we're stuck with this legacy in 2 ways. Firstly that the
default is to force everything to wait until the previous job is finished, but
more importantly that output from parallel jobs frequently collides
$ find / -ctime 5d -print &
$ find / -uid 0 -print &
// good luck working out what came from where
$ tail -f logfile.txt &
$ vi main.c
// have a nice time editing that file
GCLI should allow commands to be asynchronous and will provide UI elements to
inform the user of job completion. It will also keep asynchronous command
output contained within it's own display area.
### Output limitations
The PDP-7 had a teletype. There is something like 4 orders of magnitude more
information that can be displayed on a modern display than a 80x24 character
based console. We can use this flexibility to provide better help to the user
in entering their command.
The additional display richness can also allow interaction with result output.
Command output can include links to follow-up commands, and even ask for
additional input. (e.g. "your search returned zero results do you want to try
again with a different search string")
There is no reason why output must be static. For example, it could be
informative to see the results of an "ls" command alter given changes made by
subsequent commands. (It should be noted that there are times when historical
information is important too)
### Integration limitations
In 1970, command execution meant retrieving a program from storage, and running
it. This required minimal interaction between the command line processor and
the program being run, and was good for resource constrained systems.
This lack of interaction resulted in the processing of command line arguments
being done everywhere, when the task was better suited to command line.
We should provide metadata about the commands being run, to allow the command
line to process, interpret and provide help on the input.

View File

@ -0,0 +1,213 @@
# Developing GCLI
## About the code
The majority of the GCLI source is stored in the ``lib`` directory.
The ``docs`` directory contains documentation.
The ``scripts`` directory contains RequireJS that GCLI uses.
The ``build`` directory contains files used when creating builds.
The ``mozilla`` directory contains the mercurial patch queue of patches to apply
to mozilla-central.
The ``selenium-tests`` directory contains selenium web-page integration tests.
The source in the ``lib`` directory is split into 4 sections:
- ``lib/demo`` contains commands used in the demo page. It is not needed except
for demo purposes.
- ``lib/test`` contains a small test harness for testing GCLI.
- ``lib/gclitest`` contains tests that run in the test harness
- ``lib/gcli`` contains the actual meat
GCLI is split into a UI portion and a Model/Controller portion.
## The GCLI Model
The heart of GCLI is a ``Requisition``, which is an AST for the input. A
``Requisition`` is a command that we'd like to execute, and we're filling out
all the inputs required to execute the command.
A ``Requisition`` has a ``Command`` that is to be executed. Each Command has a
number of ``Parameter``s, each of which has a name and a type as detailed
above.
As you type, your input is split into ``Argument``s, which are then assigned to
``Parameter``s using ``Assignment``s. Each ``Assignment`` has a ``Conversion``
which stores the input argument along with the value that is was converted into
according to the type of the parameter.
There are special assignments called ``CommandAssignment`` which the
``Requisition`` uses to link to the command to execute, and
``UnassignedAssignment``used to store arguments that do not have a parameter
to be assigned to.
## The GCLI UI
There are several components of the GCLI UI. Each can have a script portion,
some template HTML and a CSS file. The template HTML is processed by
``domtemplate`` before use.
DomTemplate is fully documented in [it's own repository]
(https://github.com/joewalker/domtemplate).
The components are:
- ``Inputter`` controls the input field, processing special keyboard events and
making sure that it stays in sync with the Requisition.
- ``Completer`` updates a div that is located behind the input field and used
to display completion advice and hint highlights. It is stored in
completer.js.
- ``Display`` is responsible for containing the popup hints that are displayed
above the command line. Typically Display contains a Hinter and a RequestsView
although these are not both required. Display itself is optional, and isn't
planned for use in the first release of GCLI in Firefox.
- ``Hinter`` Is used to display input hints. It shows either a Menu or an
ArgFetch component depending on the state of the Requisition
- ``Menu`` is used initially to select the command to be executed. It can act
somewhat like the Start menu on windows.
- ``ArgFetch`` Once the command to be executed has been selected, ArgFetch
shows a 'dialog' allowing the user to enter the parameters to the selected
command.
- ``RequestsView`` Contains a set of ``RequestView`` components, each of which
displays a command that has been invoked. RequestsView is a poor name, and
should better be called ReportView
ArgFetch displays a number of Fields. There are fields for most of the Types
discussed earlier. See 'Writing Fields' above for more information.
## Testing
GCLI contains 2 test suites:
- JS level testing is run with the ``test`` command. The tests are located in
``lib/gclitest`` and they use the test runner in ``lib/test``. This is fairly
comprehensive, however it does not do UI level testing.
If writing a new test it needs to be registered in ``lib/gclitest/index``.
For an example of how to write tests, see ``lib/gclitest/testSplit.js``.
The test functions are implemented in ``lib/test/assert``.
- Browser integration tests are included in ``browser_webconsole_gcli_*.js``,
in ``toolkit/components/console/hudservice/tests/browser``. These are
run with the rest of the Mozilla test suite.
## Coding Conventions
The coding conventions for the GCLI project come from the Bespin/Skywriter and
Ace projects. They are roughly [Crockford]
(http://javascript.crockford.com/code.html) with a few exceptions and
additions:
* ``var`` does not need to be at the top of each function, we'd like to move
to ``let`` when it's generally available, and ``let`` doesn't have the same
semantic twists as ``var``.
* Strings are generally enclosed in single quotes.
* ``eval`` is to be avoided, but we don't declare it evil.
The [Google JavaScript conventions]
(https://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml) are
more detailed, we tend to deviate in:
* Custom exceptions: We generally just use ``throw new Error('message');``
* Multi-level prototype hierarchies: Allowed; we don't have ``goog.inherits()``
* ``else`` begins on a line by itself:
if (thing) {
doThis();
}
else {
doThat();
}
## Startup
Internally GCLI modules have ``startup()``/``shutdown()`` functions which are
called on module init from the top level ``index.js`` of that 'package'.
In order to initialize a package all that is needed is to require the package
index (e.g. ``require('package/index')``).
The ``shutdown()`` function was useful when GCLI was used in Bespin as part of
dynamic registration/de-registration. It is not known if this feature will be
useful in the future. So it has not been entirely removed, it may be at some
future date.
## Running the Unit Tests
Start the GCLI static server:
cd path/to/gcli
node gcli.js
Now point your browser to http://localhost:9999/localtest.html. When the page
loads the tests will be automatically run outputting to the console, or you can
enter the ``test`` command to run the unit tests.
## Contributing Code
Please could you do the following to help minimize the amount of rework that we
do:
1. Check the unit tests run correctly (see **Running the Unit Tests** above)
2. Check the code follows the style guide. At a minimum it should look like the
code around it. For more detailed notes, see **Coding Conventions** above
3. Help me review your work by using good commit comments. Which means 2 things
* Well formatted messages, i.e. 50 char summary including bug tag, followed
by a blank line followed by a more in-depth message wrapped to 72 chars
per line. This is basically the format used by the Linux Kernel. See the
[commit log](https://github.com/joewalker/gcli/commits/master) for
examples. The be extra helpful, please use the "shortdesc-BUGNUM: " if
possible which also helps in reviews.
* Commit your changes as a story. Make it easy for me to understand the
changes that you've made.
4. Sign your work. To improve tracking of who did what, we follow the sign-off
procedure used in the Linux Kernel.
The sign-off is a simple line at the end of the explanation for the
patch, which certifies that you wrote it or otherwise have the right to
pass it on as an open-source patch. The rules are pretty simple: if you
can certify the below:
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
then you just add a line saying
Signed-off-by: Random J Developer <random@developer.example.org>
using your real name (sorry, no pseudonyms or anonymous contributions.)
Thanks for wanting to contribute code.

View File

@ -0,0 +1,150 @@
# About GCLI
## GCLI is a Graphical Command Line Interpreter.
GCLI is a command line for modern computers. When command lines were invented,
computers were resource-limited, disconnected systems with slow multi-tasking
and poor displays. The design of the Unix CLI made sense in 1970, but over 40
years on, considering the pace of change, there are many improvements we can
make.
CLIs generally suffer from poor discoverability; It's hard when faced with a
blank command line to work out what to do. As a result the majority of programs
today use purely graphical user interfaces, however in doing so, they lose some
of the benefits of CLIs. CLIs are still used because generally, in the hands of
a skilled user they are faster, and have a wider range of available options.
GCLI attempts to get the best of the GUI world and the CLI world to produce
something that is both easy to use and learn as well as fast and powerful.
GCLI has a type system to help ensure that users are inputting valid commands
and to enable us to provide sensible context sensitive help. GCLI provides
integration with JavaScript rather than being an alternative (like CoffeeScript).
## History
GCLI was born as part of the
[Bespin](http://ajaxian.com/archives/canvas-for-a-text-editor) project and was
[discussed at the time](http://j.mp/bespin-cli). The command line component
survived the rename of Bepsin to Skywriter and the merger with Ace, got a name
of it's own (Cockpit) which didn't last long before the project was named GCLI.
It is now being used in the Firefox's web console where it doesn't have a
separate identity but it's still called GCLI outside of Firefox. It is also
used in [Eclipse Orion](http://www.eclipse.org/orion/).
## Environments
GCLI is designed to work in a number of environments:
1. As a component of Firefox developer tools.
2. As an adjunct to Orion/Ace and other online editors.
3. As a plugin to any web-page wishing to provide its own set of commands.
4. As part of a standalone web browser extension with it's own set of commands.
## Related Pages
Other sources of GCLI documentation:
- [Writing Commands](writing-commands.md)
- [Writing Types](writing-types.md)
- [Developing GCLI](developing-gcli.md)
- [Writing Tests](writing-tests.md) / [Running Tests](running-tests.md)
- [The Design of GCLI](design.md)
- Source
- The most up-to-date source is in [this Github repository](https://github.com/joewalker/gcli/).
- When a feature is 'done' it's merged into the [Mozilla clone](https://github.com/mozilla/gcli/).
- From which it flows into [Mozilla Central](https://hg.mozilla.org/mozilla-central/file/tip/browser/devtools/commandline).
- [Demo of GCLI](http://mozilla.github.com/gcli/) with an arbitrary set of demo
commands
- Other Documentation
- [Embedding docs](https://github.com/mozilla/gcli/blob/master/docs/index.md)
- [Status page](http://mozilla.github.com/devtools/2011/status.html#gcli)
## Accessibility
GCLI uses ARIA roles to guide a screen-reader as to the important sections to
voice. We welcome [feedback on how these roles are implemented](https://bugzilla.mozilla.org/enter_bug.cgi?product=Firefox&component=Developer+Tools:+Graphic+Commandline+and+Toolbar&rep_platform=All&op_sys=All&short_desc=GCLI).
The command line uses TAB as a method of completing current input, this
prevents use of TAB for keyboard navigation. Instead of using TAB to move to
the next field you can use F6. In addition to F6, ALT+TAB, CTRL+TAB, META+TAB
make an attempt to move the focus on. How well this works depends on your
OS/browser combination.
## Embedding GCLI
There are 3 basic steps in using GCLI in your system.
1. Import a GCLI JavaScript file.
For serious use of GCLI you are likely to be creating a custom build (see
below) however if you just want to have a quick play, you can use
``gcli-uncompressed.js`` from [the gh-pages branch of GCLI]
(https://github.com/mozilla/gcli/tree/gh-pages)
Just place the following wherever you place your script files.
<script src="path/to/gcli-uncompressed.js" type="text/javascript"></script>
2. Having imported GCLI, we need to tell it where to display. The simplest
method is to include an elements with the id of ``gcli-input`` and
``gcli-display``.
<input id="gcli-input" type="text"/>
<div id="gcli-display"></div>
3. Tell GCLI what commands to make available. See the sections on Writing
Commands, Writing Types and Writing Fields for more information.
GCLI uses the CommonJS AMD format for it's files, so a 'require' statement
is needed to get started.
require([ 'gcli/index' ], function(gcli) {
gcli.addCommand(...); // Register custom commands
gcli.createTerminal(); // Create a user interface
});
The createTerminal() function takes an ``options`` objects which allows
customization. At the current time the documentation of these object is left
to the source.
## Backwards Compatibility
The goals of the GCLI project are:
- Aim for very good backwards compatibility with code required from an
'index' module. This means we will not break code without a cycle of
deprecation warnings.
There are currently 3 'index' modules:
- gcli/index (all you need to get started with GCLI)
- demo/index (a number of demo commands)
- gclitest/index (GCLI test suite)
Code from these modules uses the module pattern to prevent access to internal
functions, so in essence, if you can get to it from an index module, you
should be ok.
- We try to avoid needless change to other modules, however we don't make any
promises, and don't provide a deprecation cycle.
Code from other modules uses classes rather than modules, so member variables
are exposed. Many classes mark private members using the `_underscorePrefix`
pattern. Particular care should be taken if access is needed to a private
member.
## Creating Custom Builds
GCLI uses [DryIce](https://github.com/mozilla/dryice) to create custom builds.
If dryice is installed (``npm install .``) then you can create a built
version of GCLI simply using ``node gcli.js standard``. DryIce supplies a custom
module loader to replace RequireJS for built applications.
The build will be output to the ``built`` directory. The directory will be
created if it doesn't exist.

View File

@ -0,0 +1,71 @@
# Running Tests
GCLI has a test suite that can be run in a number of different environments.
Some of the tests don't work in all environments. These should be automatically
skipped when not applicable.
## Web
Running a limited set of test from the web is the easiest. Simply load
'localtest.html' and the unit tests should be run automatically, with results
displayed on the console. Tests can be re-run using the 'test' command.
It also creates a function 'testCommands()' to be run at a JS prompt, which
enables the test commands for debugging purposes.
## Firefox
GCLI's test suite integrates with Mochitest and runs automatically on each test
run. Dryice packages the tests to format them for the Firefox build system.
For more information about running Mochitest on Firefox (including GCLI) see
[the MDN, Mochitest docs](https://developer.mozilla.org/en/Mochitest)
# Node
Running the test suite under node can be done as follows:
$ node gcli.js test
Or, using the `test` command:
$ node gcli.js
Serving GCLI to http://localhost:9999/
This is also a limited GCLI prompt.
Type 'help' for a list of commands, CTRL+C twice to exit:
: test
testCli: Pass (funcs=9, checks=208)
testCompletion: Pass (funcs=1, checks=139)
testExec: Pass (funcs=1, checks=133)
testHistory: Pass (funcs=3, checks=13)
....
Summary: Pass (951 checks)
# Phantom
The GCLI test suite can also be run under PhantomJS as follows:
$ phantomjs ./phantom-test.js
Summary: Pass (4289 checks)
Finished running unit tests. (total 3.843s, ave response time 3.36ms, ...)
# Travis CI
GCLI check-ins are automatically tested by [Travis CI](https://travis-ci.org/joewalker/gcli).
# Test Case Generation
GCLI can generate test cases automagically. Load ```localtest.html```, type a
command to be tested into GCLI, and the press F2. GCLI will output to the
console a template test case for the entered command.

View File

@ -0,0 +1,755 @@
# Writing Commands
## Basics
GCLI has opinions about how commands should be written, and it encourages you
to do The Right Thing. The opinions are based on helping users convert their
intentions to commands and commands to what's actually going to happen.
- Related commands should be sub-commands of a parent command. One of the goals
of GCLI is to support a large number of commands without things becoming
confusing, this will require some sort of namespacing or there will be
many people wanting to implement the ``add`` command. This style of
writing commands has become common place in Unix as the number of commands
has gone up.
The ```context``` command allows users to focus on a parent command, promoting
its sub-commands above others.
- Each command should do exactly and only one thing. An example of a Unix
command that breaks this principle is the ``tar`` command
$ tar -zcf foo.tar.gz .
$ tar -zxf foo.tar.gz .
These 2 commands do exactly opposite things. Many a file has died as a result
of a x/c typo. In GCLI this would be better expressed:
$ tar create foo.tar.gz -z .
$ tar extract foo.tar.gz -z .
There may be commands (like tar) which have enough history behind them
that we shouldn't force everyone to re-learn a new syntax. The can be achieved
by having a single string parameter and parsing the input in the command)
- Avoid errors. We try to avoid the user having to start again with a command
due to some problem. The majority of problems are simple typos which we can
catch using command metadata, but there are 2 things command authors can do
to prevent breakage.
- Where possible avoid the need to validate command line parameters in the
exec function. This can be done by good parameter design (see 'do exactly
and only one thing' above)
- If there is an obvious fix for an unpredictable problem, offer the
solution in the command output. So rather than use request.error (see
Request Object below) output some HTML which contains a link to a fixed
command line.
Currently these concepts are not enforced at a code level, but they could be in
the future.
## How commands work
This is how to create a basic ``greet`` command:
gcli.addCommand({
name: 'greet',
description: 'Show a greeting',
params: [
{
name: 'name',
type: 'string',
description: 'The name to greet'
}
],
returnType: 'string',
exec: function(args, context) {
return 'Hello, ' + args.name;
}
});
This command is used as follows:
: greet Joe
Hello, Joe
Some terminology that isn't always obvious: a function has 'parameters', and
when you call a function, you pass 'arguments' to it.
## Internationalization (i18n)
There are several ways that GCLI commands can be localized. The best method
depends on what context you are writing your command for.
### Firefox Embedding
GCLI supports Mozilla style localization. To add a command that will only ever
be used embedded in Firefox, this is the way to go. Your strings should be
stored in ``browser/locales/en-US/chrome/browser/devtools/gclicommands.properties``,
And you should access them using ``gcli.lookup(...)`` or ``gcli.lookupFormat()``
For examples of existing commands, take a look in
``browser/devtools/webconsole/GcliCommands.jsm``, which contains most of the
current GCLI commands. If you will be adding a number of new commands, then
consider starting a new JSM.
Your command will then look something like this:
gcli.addCommand({
name: 'greet',
description: gcli.lookup("greetDesc")
...
});
### Web Commands
There are 2 ways to provide translated strings for web use. The first is to
supply the translated strings in the description:
gcli.addCommand({
name: 'greet',
description: {
'root': 'Show a greeting',
'fr-fr': 'Afficher un message d'accueil',
'de-de': 'Zeige einen Gruß',
'gk-gk': 'Εμφάνιση ένα χαιρετισμό',
...
}
...
});
Each description should contain at least a 'root' entry which is the
default if no better match is found. This method has the benefit of being
compact and simple, however it has the significant drawback of being wasteful
of memory and bandwidth to transmit and store a significant number of strings,
the majority of which will never be used.
More efficient is to supply a lookup key and ask GCLI to lookup the key from an
appropriate localized strings file:
gcli.addCommand({
name: 'greet',
description: { 'key': 'demoGreetingDesc' }
...
});
For web usage, the central store of localized strings is
``lib/gcli/nls/strings.js``. Other string files can be added using the
``l10n.registerStringsSource(...)`` function.
This method can be used both in Firefox and on the Web (see the help command
for an example). However this method has the drawback that it will not work
with DryIce built files until we fix bug 683844.
## Default argument values
The ``greet`` command requires the entry of the ``name`` parameter. This
parameter can be made optional with the addition of a ``defaultValue`` to the
parameter:
gcli.addCommand({
name: 'greet',
description: 'Show a message to someone',
params: [
{
name: 'name',
type: 'string',
description: 'The name to greet',
defaultValue: 'World!'
}
],
returnType: 'string',
exec: function(args, context) {
return "Hello, " + args.name;
}
});
Now we can also use the ``greet`` command as follows:
: greet
Hello, World!
## Positional vs. named arguments
Arguments can be entered either positionally or as named arguments. Generally
users will prefer to type the positional version, however the named alternative
can be more self documenting.
For example, we can also invoke the greet command as follows:
: greet --name Joe
Hello, Joe
## Short argument names
GCLI allows you to specify a 'short' character for any parameter:
gcli.addCommand({
name: 'greet',
params: [
{
name: 'name',
short: 'n',
type: 'string',
...
}
],
...
});
This is used as follows:
: greet -n Fred
Hello, Fred
Currently GCLI does not allow short parameter merging (i.e. ```ls -la```)
however this is planned.
## Parameter types
Initially the available types are:
- string
- boolean
- number
- selection
- delegate
- date
- array
- file
- node
- nodelist
- resource
- command
- setting
This list can be extended. See [Writing Types](writing-types.md) on types for
more information.
The following examples assume the following definition of the ```greet```
command:
gcli.addCommand({
name: 'greet',
params: [
{ name: 'name', type: 'string' },
{ name: 'repeat', type: 'number' }
],
...
});
Parameters can be specified either with named arguments:
: greet --name Joe --repeat 2
And sometimes positionally:
: greet Joe 2
Parameters can be specified positionally if they are considered 'important'.
Unimportant parameters must be specified with a named argument.
Named arguments can be specified anywhere on the command line (after the
command itself) however positional arguments must be in order. So
these examples are the same:
: greet --name Joe --repeat 2
: greet --repeat 2 --name Joe
However (obviously) these are not the same:
: greet Joe 2
: greet 2 Joe
(The second would be an error because 'Joe' is not a number).
Named arguments are assigned first, then the remaining arguments are assigned
to the remaining parameters. So the following is valid and unambiguous:
: greet 2 --name Joe
Positional parameters quickly become unwieldy with long parameter lists so we
recommend only having 2 or 3 important parameters. GCLI provides hints for
important parameters more obviously than unimportant ones.
Parameters are 'important' if they are not in a parameter group. The easiest way
to achieve this is to use the ```option: true``` property.
For example, using:
gcli.addCommand({
name: 'greet',
params: [
{ name: 'name', type: 'string' },
{ name: 'repeat', type: 'number', option: true, defaultValue: 1 }
],
...
});
Would mean that this is an error
: greet Joe 2
You would instead need to do the following:
: greet Joe --repeat 2
For more on parameter groups, see below.
In addition to being 'important' and 'unimportant' parameters can also be
optional. If is possible to be important and optional, but it is not possible
to be unimportant and non-optional.
Parameters are optional if they either:
- Have a ```defaultValue``` property
- Are of ```type=boolean``` (boolean arguments automatically default to being false)
There is currently no way to make parameters mutually exclusive.
## Selection types
Parameters can have a type of ``selection``. For example:
gcli.addCommand({
name: 'greet',
params: [
{ name: 'name', ... },
{
name: 'lang',
description: 'In which language should we greet',
type: { name: 'selection', data: [ 'en', 'fr', 'de', 'es', 'gk' ] },
defaultValue: 'en'
}
],
...
});
GCLI will enforce that the value of ``arg.lang`` was one of the values
specified. Alternatively ``data`` can be a function which returns an array of
strings.
The ``data`` property is useful when the underlying type is a string but it
doesn't work when the underlying type is something else. For this use the
``lookup`` property as follows:
type: {
name: 'selection',
lookup: {
'en': Locale.EN,
'fr': Locale.FR,
...
}
},
Similarly, ``lookup`` can be a function returning the data of this type.
## Number types
Number types are mostly self explanatory, they have one special property which
is the ability to specify upper and lower bounds for the number:
gcli.addCommand({
name: 'volume',
params: [
{
name: 'vol',
description: 'How loud should we go',
type: { name: 'number', min: 0, max: 11 }
}
],
...
});
You can also specify a ``step`` property which specifies by what amount we
should increment and decrement the values. The ``min``, ``max``, and ``step``
properties are used by the command line when up and down are pressed and in
the input type of a dialog generated from this command.
## Delegate types
Delegate types are needed when the type of some parameter depends on the type
of another parameter. For example:
: set height 100
: set name "Joe Walker"
We can achieve this as follows:
gcli.addCommand({
name: 'set',
params: [
{
name: 'setting',
type: { name: 'selection', values: [ 'height', 'name' ] }
},
{
name: 'value',
type: {
name: 'delegate',
delegateType: function() { ... }
}
}
],
...
});
Several details are left out of this example, like how the delegateType()
function knows what the current setting is. See the ``pref`` command for an
example.
## Array types
Parameters can have a type of ``array``. For example:
gcli.addCommand({
name: 'greet',
params: [
{
name: 'names',
type: { name: 'array', subtype: 'string' },
description: 'The names to greet',
defaultValue: [ 'World!' ]
}
],
...
exec: function(args, context) {
return "Hello, " + args.names.join(', ') + '.';
}
});
This would be used as follows:
: greet Fred Jim Shiela
Hello, Fred, Jim, Shiela.
Or using named arguments:
: greet --names Fred --names Jim --names Shiela
Hello, Fred, Jim, Shiela.
There can only be one ungrouped parameter with an array type, and it must be
at the end of the list of parameters (i.e. just before any parameter groups).
This avoids confusion as to which parameter an argument should be assigned.
## Sub-commands
It is common for commands to be groups into those with similar functionality.
Examples include virtually all VCS commands, ``apt-get``, etc. There are many
examples of commands that should be structured as in a sub-command style -
``tar`` being the obvious example, but others include ``crontab``.
Groups of commands are specified with the top level command not having an
exec function:
gcli.addCommand({
name: 'tar',
description: 'Commands to manipulate archives',
});
gcli.addCommand({
name: 'tar create',
description: 'Create a new archive',
exec: function(args, context) { ... },
...
});
gcli.addCommand({
name: 'tar extract',
description: 'Extract from an archive',
exec: function(args, context) { ... },
...
});
## Parameter groups
Parameters can be grouped into sections.
There are 3 ways to assign a parameter to a group.
The simplest uses ```option: true``` to put a parameter into the default
'Options' group:
gcli.addCommand({
name: 'greet',
params: [
{ name: 'repeat', type: 'number', option: true }
],
...
});
The ```option``` property can also take a string to use an alternative parameter
group:
gcli.addCommand({
name: 'greet',
params: [
{ name: 'repeat', type: 'number', option: 'Advanced' }
],
...
});
An example of how this can be useful is 'git' which categorizes parameters into
'porcelain' and 'plumbing'.
Finally, parameters can be grouped together as follows:
gcli.addCommand({
name: 'greet',
params: [
{ name: 'name', type: 'string', description: 'The name to greet' },
{
group: 'Advanced Options',
params: [
{ name: 'repeat', type: 'number', defaultValue: 1 },
{ name: 'debug', type: 'boolean' }
]
}
],
...
});
This could be used as follows:
: greet Joe --repeat 2 --debug
About to send greeting
Hello, Joe
Hello, Joe
Done!
Parameter groups must come after non-grouped parameters 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.
## Command metadata
Each command should have the following properties:
- A string ``name``.
- A short ``description`` string. Generally no more than 20 characters without
a terminating period/fullstop.
- A function to ``exec``ute. (Optional for the parent containing sub-commands)
See below for more details.
And optionally the following extra properties:
- A declaration of the accepted ``params``.
- A ``hidden`` property to stop the command showing up in requests for help.
- A ``context`` property which defines the scope of the function that we're
calling. Rather than simply call ``exec()``, we do ``exec.call(context)``.
- A ``manual`` property which allows a fuller description of the purpose of the
command.
- A ``returnType`` specifying how we should handle the value returned from the
exec function.
The ``params`` property is an array of objects, one for each parameter. Each
parameter object should have the following 3 properties:
- A string ``name``.
- A short string ``description`` as for the command.
- A ``type`` which refers to an existing Type (see Writing Types).
Optionally each parameter can have these properties:
- A ``defaultValue`` (which should be in the type specified in ``type``).
The defaultValue will be used when there is no argument supplied for this
parameter on the command line.
If the parameter has a ``defaultValue``, other than ``undefined`` then the
parameter is optional, and if unspecified on the command line, the matching
argument will have this value when the function is called.
If ``defaultValue`` is missing, or if it is set to ``undefined``, then the
system will ensure that a value is provided before anything is executed.
There are 2 special cases:
- If the type is ``selection``, then defaultValue must not be undefined.
The defaultValue must either be ``null`` (meaning that a value must be
supplied by the user) or one of the selection values.
- If the type is ``boolean``, then ``defaultValue:false`` is implied and
can't be changed. Boolean toggles are assumed to be off by default, and
should be named to match.
- A ``manual`` property for parameters is exactly analogous to the ``manual``
property for commands - descriptive text that is longer than than 20
characters.
## The Command Function (exec)
The parameters to the exec function are designed to be useful when you have a
large number of parameters, and to give direct access to the environment (if
used).
gcli.addCommand({
name: 'echo',
description: 'The message to display.',
params: [
{
name: 'message',
type: 'string',
description: 'The message to display.'
}
],
returnType: 'string',
exec: function(args, context) {
return args.message;
}
});
The ``args`` object contains the values specified on the params section and
provided on the command line. In this example it would contain the message for
display as ``args.message``.
The ``context`` object has the following signature:
{
environment: ..., // environment object passed to createTerminal()
exec: ..., // function to execute a command
update: ..., // function to alter the text of the input area
createView: ..., // function to help creating rich output
defer: ..., // function to create a deferred promise
}
The ``environment`` object is opaque to GCLI. It can be used for providing
arbitrary data to your commands about their environment. It is most useful
when more than one command line exists on a page with similar commands in both
which should act in their own ways.
An example use for ``environment`` would be a page with several tabs, each
containing an editor with a command line. Commands executed in those editors
should apply to the relevant editor.
The ``environment`` object is passed to GCLI at startup (probably in the
``createTerminal()`` function).
The ``document`` object is also passed to GCLI at startup. In some environments
(e.g. embedded in Firefox) there is no global ``document``. This object
provides a way to create DOM nodes.
``defer()`` allows commands to execute asynchronously.
## Returning data
The command meta-data specifies the type of data returned by the command using
the ``returnValue`` setting.
``returnValue`` processing is currently functioning, but incomplete, and being
tracked in [Bug 657595](http://bugzil.la/657595). Currently you should specify
a ``returnType`` of ``string`` or ``html``. If using HTML, you can return
either an HTML string or a DOM node.
In the future, JSON will be strongly encouraged as the return type, with some
formatting functions to convert the JSON to HTML.
Asynchronous output is achieved using a promise created from the ``context``
parameter: ``context.defer()``.
Some examples of this is practice:
{ returnType: "string" }
...
return "example";
GCLI interprets the output as a plain string. It will be escaped before display
and available as input to other commands as a plain string.
{ returnType: "html" }
...
return "<p>Hello</p>";
GCLI will interpret this as HTML, and parse it for display.
{ returnType: "dom" }
...
return util.createElement(context.document, 'div');
``util.createElement`` is a utility to ensure use of the XHTML namespace in XUL
and other XML documents. In an HTML document it's functionally equivalent to
``context.document.createElement('div')``. If your command is likely to be used
in Firefox or another XML environment, you should use it. You can import it
with ``var util = require('util/util');``.
GCLI will use the returned HTML element as returned. See notes on ``context``
above.
{ returnType: "number" }
...
return 42;
GCLI will display the element in a similar way to a string, but it the value
will be available to future commands as a number.
{ returnType: "date" }
...
return new Date();
{ returnType: "file" }
...
return new File();
Both these examples return data as a given type, for which a converter will
be required before the value can be displayed. The type system is likely to
change before this is finalized. Please contact the author for more
information.
{ returnType: "string" }
...
var deferred = context.defer();
setTimeout(function() {
deferred.resolve("hello");
}, 500);
return deferred.promise;
Errors can be signaled by throwing an exception. GCLI will display the message
property (or the toString() value if there is no message property). (However
see *3 principles for writing commands* above for ways to avoid doing this).
## Specifying Types
Types are generally specified by a simple string, e.g. ``'string'``. For most
types this is enough detail. There are a number of exceptions:
* Array types. We declare a parameter to be an array of things using ``[]``,
for example: ``number[]``.
* Selection types. There are 3 ways to specify the options in a selection:
* Using a lookup map
type: {
name: 'selection',
lookup: { one:1, two:2, three:3 }
}
(The boolean type is effectively just a selection that uses
``lookup:{ 'true': true, 'false': false }``)
* Using given strings
type: {
name: 'selection',
data: [ 'left', 'center', 'right' ]
}
* Using named objects, (objects with a ``name`` property)
type: {
name: 'selection',
data: [
{ name: 'Google', url: 'http://www.google.com/' },
{ name: 'Microsoft', url: 'http://www.microsoft.com/' },
{ name: 'Yahoo', url: 'http://www.yahoo.com/' }
]
}
* Delegate type. It is generally best to inherit from Delegate in order to
provide a customization of this type. See settingValue for an example.
See below for more information.

View File

@ -0,0 +1,20 @@
# Writing Tests
There are several sources of GCLI tests and several environments in which they
are run.
The majority of GCLI tests are stored in
[this repository](https://github.com/joewalker/gcli/) in files named like
```./lib/gclitest/test*.js```. These tests run in Firefox, Chrome, Opera,
PhantomJS, and NodeJS/JsDom
See [Running Tests](running-tests.md) for further details.
GCLI comes with a generic unit test harness (in ```./lib/test/```) and a
set of helpers for creating GCLI tests (in ```./lib/gclitest/helpers.js```).
# GCLI tests in Firefox
The build process converts the GCLI tests to run under Mochitest inside the
Firefox unit tests. It also adds some

View File

@ -0,0 +1,106 @@
# Writing Types
Commands are a fundamental building block because they are what the users
directly interacts with, however they are built on ``Type``s. There are a
number of built in types:
* string. This is a JavaScript string
* number. A JavaScript number
* boolean. A Javascript boolean
* selection. This is an selection from a number of alternatives
* delegate. This type could change depending on other factors, but is well
defined when one of the conversion routines is called.
There are a number of additional types defined by Pilot and GCLI as
extensions to the ``selection`` and ``delegate`` types
* setting. One of the defined settings
* settingValue. A value that can be applied to an associated setting.
* command. One of the defined commands
Most of our types are 'static' e.g. there is only one type of 'string', however
some types like 'selection' and 'delegate' are customizable.
All types must inherit from Type and have the following methods:
/**
* Convert the given <tt>value</tt> to a string representation.
* Where possible, there should be round-tripping between values and their
* string representations.
*/
stringify: function(value) { return 'string version of value'; },
/**
* Convert the given <tt>str</tt> to an instance of this type.
* Where possible, there should be round-tripping between values and their
* string representations.
* @return Conversion
*/
parse: function(str) { return new Conversion(...); },
/**
* 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 'delegate' need extra data, however this function returns
* only the name, not the extra data.
* <p>In old bespin, equality was based on the name. This may turn out to be
* important in Ace too.
*/
name: 'example',
In addition, defining the following functions can be helpful, although Type
contains default implementations:
* increment(value)
* decrement(value)
Type, Conversion and Status are all declared by canon.js.
The values produced by the parse function can be of any type, but if you are
producing your own, you are strongly encouraged to include properties called
``name`` and ``description`` where it makes sense. There are a number of
places in GCLI where the UI will be able to provide better help to users if
your values include these properties.
# Writing Fields
Fields are visual representations of types. For simple types like string it is
enough to use ``<input type=...>``, however more complex types we may wish to
provide a custom widget to allow the user to enter values of the given type.
This is an example of a very simple new password field type:
function PasswordField(doc) {
this.doc = doc;
}
PasswordField.prototype = Object.create(Field.prototype);
PasswordField.prototype.createElement = function(assignment) {
this.assignment = assignment;
this.input = dom.createElement(this.doc, 'input');
this.input.type = 'password';
this.input.value = assignment.arg ? assignment.arg.text : '';
this.onKeyup = function() {
this.assignment.setValue(this.input.value);
}.bind(this);
this.input.addEventListener('keyup', this.onKeyup, false);
this.onChange = function() {
this.input.value = this.assignment.arg.text;
};
this.assignment.onAssignmentChange.add(this.onChange, this);
return this.input;
};
PasswordField.prototype.destroy = function() {
this.input.removeEventListener('keyup', this.onKeyup, false);
this.assignment.onAssignmentChange.remove(this.onChange, this);
};
PasswordField.claim = function(type) {
return type.name === 'password' ? Field.claim.MATCH : Field.claim.NO_MATCH;
};

View File

@ -0,0 +1,183 @@
/*
* 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';
var api = require('../api');
var connectors = require('./connectors');
var Canon = require('../commands/commands').Canon;
var Types = require('../types/types').Types;
// Patch-up IE9
require('../util/legacy');
/*
* GCLI is built from a number of components (called items) composed as
* required for each environment.
* When adding to or removing from this list, we should keep the basics in sync
* with the other environments.
* See:
* - lib/gcli/index.js: Generic basic set (without commands)
* - lib/gcli/demo.js: Adds demo commands to basic set for use in web demo
* - gcli.js: Add commands to basic set for use in Node command line
* - lib/gcli/index.js: (mozmaster branch) From scratch listing for Firefox
* - lib/gcli/connectors/index.js: Client only items when executing remotely
* - lib/gcli/connectors/direct.js: Test items for connecting to in-process GCLI
*/
var items = [
// First we need to add the local types which other types depend on
require('../types/delegate').items,
require('../types/selection').items,
require('../types/array').items,
require('../types/boolean').items,
require('../types/command').items,
require('../types/date').items,
require('../types/file').items,
require('../types/javascript').items,
require('../types/node').items,
require('../types/number').items,
require('../types/resource').items,
require('../types/setting').items,
require('../types/string').items,
require('../fields/delegate').items,
require('../fields/selection').items,
require('../ui/intro').items,
require('../ui/focus').items,
require('../converters/converters').items,
require('../converters/basic').items,
require('../converters/html').items,
require('../converters/terminal').items,
require('../languages/command').items,
require('../languages/javascript').items,
require('./direct').items,
// require('./rdp').items, // Firefox remote debug protocol
require('./websocket').items,
require('./xhr').items,
require('../commands/context').items,
].reduce(function(prev, curr) { return prev.concat(curr); }, []);
/**
* These are the commands stored on the remote side that have converters which
* we'll need to present the data
*/
var requiredConverters = [
require('../cli').items,
require('../commands/clear').items,
require('../commands/connect').items,
require('../commands/exec').items,
require('../commands/global').items,
require('../commands/help').items,
require('../commands/intro').items,
require('../commands/lang').items,
require('../commands/preflist').items,
require('../commands/pref').items,
require('../commands/test').items,
].reduce(function(prev, curr) { return prev.concat(curr); }, [])
.filter(function(item) { return item.item === 'converter'; });
/**
* Connect to a remote system and setup the canon/types/converters etc needed
* to make it all work
*/
exports.connect = function(options) {
options = options || {};
var gcli = api.getApi();
// Ugly hack, to aid testing
exports.api = gcli;
options.types = gcli.types = new Types();
options.canon = gcli.canon = new Canon({ types: gcli.types });
gcli.addItems(items);
gcli.addItems(requiredConverters);
var connector = connectors.get(options.connector);
return connector.connect(options.url).then(function(connection) {
options.connection = connection;
connection.on('canonChanged', function(specs) {
exports.addItems(gcli, specs, connection);
});
return connection.call('specs').then(function(specs) {
exports.addItems(gcli, specs, connection);
return connection;
});
});
};
exports.addItems = function(gcli, specs, connection) {
exports.removeRemoteItems(gcli, connection);
var remoteItems = exports.addLocalFunctions(specs, connection);
gcli.addItems(remoteItems);
};
/**
* Take the data from the 'specs' command (or the 'canonChanged' event) and
* add function to proxy the execution back over the connection
*/
exports.addLocalFunctions = function(specs, connection) {
// Inject an 'exec' function into the commands, and the connection into
// all the remote types
specs.forEach(function(commandSpec) {
//
commandSpec.connection = connection;
commandSpec.params.forEach(function(param) {
param.type.connection = connection;
});
if (!commandSpec.isParent) {
commandSpec.exec = function(args, context) {
var data = {
typed: (context.prefix ? context.prefix + ' ' : '') + context.typed
};
return connection.call('execute', data).then(function(reply) {
var typedData = context.typedData(reply.type, reply.data);
if (!reply.error) {
return typedData;
}
else {
throw typedData;
}
});
};
}
commandSpec.isProxy = true;
});
return specs;
};
exports.removeRemoteItems = function(gcli, connection) {
gcli.canon.getCommands().forEach(function(command) {
if (command.connection === connection) {
gcli.canon.removeCommand(command);
}
});
};

View File

@ -0,0 +1,56 @@
/*
* 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';
/**
* This is a quick and dirty stub that allows us to write code in remoted.js
* that looks like gcli.js
*/
exports.method = function(func, spec) {
// An array of strings, being the names of the parameters
var argSpecs = [];
if (spec.request != null) {
Object.keys(spec.request).forEach(function(name) {
var arg = spec.request[name];
argSpecs[arg.index] = name;
});
}
return function(data) {
var args = (data == null) ?
[] :
argSpecs.map(function(name) { return data[name]; });
return func.apply(this, args);
};
};
var Arg = exports.Arg = function(index, type) {
if (this == null) {
return new Arg(index, type);
}
this.index = index;
this.type = type;
};
var RetVal = exports.RetVal = function(type) {
if (this == null) {
return new RetVal(type);
}
this.type = type;
};

View File

@ -0,0 +1,147 @@
/*
* 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';
var Cu = require('chrome').Cu;
var debuggerSocketConnect = Cu.import('resource://gre/modules/devtools/dbg-client.jsm', {}).debuggerSocketConnect;
var DebuggerClient = Cu.import('resource://gre/modules/devtools/dbg-client.jsm', {}).DebuggerClient;
var promise = require('../util/promise');
var Connection = require('./connectors').Connection;
/**
* What port should we use by default?
*/
Object.defineProperty(exports, 'defaultPort', {
get: function() {
var Services = Cu.import('resource://gre/modules/Services.jsm', {}).Services;
try {
return Services.prefs.getIntPref('devtools.debugger.chrome-debugging-port');
}
catch (ex) {
console.error('Can\'t use default port from prefs. Using 9999');
return 9999;
}
},
enumerable: true
});
exports.items = [
{
item: 'connector',
name: 'rdp',
connect: function(url) {
return RdpConnection.create(url);
}
}
];
/**
* RdpConnection uses the Firefox Remote Debug Protocol
*/
function RdpConnection(url) {
throw new Error('Use RdpConnection.create');
}
/**
* Asynchronous construction
*/
RdpConnection.create = function(url) {
this.host = url;
this.port = undefined; // TODO: Split out the port number
this.requests = {};
this.nextRequestId = 0;
this._emit = this._emit.bind(this);
var deferred = promise.defer();
this.transport = debuggerSocketConnect(this.host, this.port);
this.client = new DebuggerClient(this.transport);
this.client.connect(function() {
this.client.listTabs(function(response) {
this.actor = response.gcliActor;
deferred.resolve();
}.bind(this));
}.bind(this));
return deferred.promise;
};
RdpConnection.prototype = Object.create(Connection.prototype);
RdpConnection.prototype.call = function(command, data) {
var deferred = promise.defer();
var request = { to: this.actor, type: command, data: data };
this.client.request(request, function(response) {
deferred.resolve(response.commandSpecs);
});
return deferred.promise;
};
RdpConnection.prototype.disconnect = function() {
var deferred = promise.defer();
this.client.close(function() {
deferred.resolve();
});
delete this._emit;
return deferred.promise;
};
/**
* A Request is a command typed at the client which lives until the command
* has finished executing on the server
*/
function Request(actor, typed, args) {
this.json = {
to: actor,
type: 'execute',
typed: typed,
args: args,
requestId: 'id-' + Request._nextRequestId++,
};
this._deferred = promise.defer();
this.promise = this._deferred.promise;
}
Request._nextRequestId = 0;
/**
* Called by the connection when a remote command has finished executing
* @param error boolean indicating output state
* @param type the type of the returned data
* @param data the data itself
*/
Request.prototype.complete = function(error, type, data) {
this._deferred.resolve({
error: error,
type: type,
data: data
});
};

View File

@ -0,0 +1,272 @@
/*
* 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.
*/
/* jshint quotmark:false, newcap:false */
'use strict';
var promise = require('../util/promise');
var host = require('../util/host');
var fileparser = require('../util/fileparser');
var protocol = require('./protocol');
var method = protocol.method;
var Arg = protocol.Arg;
var RetVal = protocol.RetVal;
/**
* Provide JSON mapping services to remote functionality of a Requisition
*/
var Remoter = exports.Remoter = function(requisition) {
this.requisition = requisition;
this._listeners = [];
};
/**
* Add a new listener
*/
Remoter.prototype.addListener = function(action) {
var listener = {
action: action,
caller: function() {
action('canonChanged', this.requisition.canon.getCommandSpecs());
}.bind(this)
};
this._listeners.push(listener);
this.requisition.canon.onCanonChange.add(listener.caller);
};
/**
* Remove an existing listener
*/
Remoter.prototype.removeListener = function(action) {
var listener;
this._listeners = this._listeners.filter(function(li) {
if (li.action === action) {
listener = li;
return false;
}
return true;
});
if (listener == null) {
throw new Error('action not a known listener');
}
this.requisition.canon.onCanonChange.remove(listener.caller);
};
/**
* These functions are designed to be remoted via RDP/XHR/websocket, etc
*/
Remoter.prototype.exposed = {
/**
* Retrieve a list of the remotely executable commands
*/
specs: method(function() {
return this.requisition.canon.getCommandSpecs();
}, {
request: {},
response: RetVal("json")
}),
/**
* Execute a GCLI command
* @return a promise of an object with the following properties:
* - data: The output of the command
* - type: The type of the data to allow selection of a converter
* - error: True if the output was considered an error
*/
execute: method(function(typed) {
return this.requisition.updateExec(typed).then(function(output) {
return output.toJson();
});
}, {
request: {
typed: Arg(0, "string") // The command string
},
response: RetVal("json")
}),
/**
* Get the state of an input string. i.e. requisition.getStateData()
*/
state: method(function(typed, start, rank) {
return this.requisition.update(typed).then(function() {
return this.requisition.getStateData(start, rank);
}.bind(this));
}, {
request: {
typed: Arg(0, "string"), // The command string
start: Arg(1, "number"), // Cursor start position
rank: Arg(2, "number") // The prediction offset (# times UP/DOWN pressed)
},
response: RetVal("json")
}),
/**
* Call type.parse to check validity. Used by the remote type
* @return a promise of an object with the following properties:
* - status: Of of the following strings: VALID|INCOMPLETE|ERROR
* - message: The message to display to the user
* - predictions: An array of suggested values for the given parameter
*/
typeparse: method(function(typed, param) {
return this.requisition.update(typed).then(function() {
var assignment = this.requisition.getAssignment(param);
return promise.resolve(assignment.predictions).then(function(predictions) {
return {
status: assignment.getStatus().toString(),
message: assignment.message,
predictions: predictions
};
});
}.bind(this));
}, {
request: {
typed: Arg(0, "string"), // The command string
param: Arg(1, "string") // The name of the parameter to parse
},
response: RetVal("json")
}),
/**
* Get the incremented value of some type
* @return a promise of a string containing the new argument text
*/
typeincrement: method(function(typed, param) {
return this.requisition.update(typed).then(function() {
var assignment = this.requisition.getAssignment(param);
return this.requisition.increment(assignment).then(function() {
var arg = assignment.arg;
return arg == null ? undefined : arg.text;
});
});
}, {
request: {
typed: Arg(0, "string"), // The command string
param: Arg(1, "string") // The name of the parameter to parse
},
response: RetVal("string")
}),
/**
* See typeincrement
*/
typedecrement: method(function(typed, param) {
return this.requisition.update(typed).then(function() {
var assignment = this.requisition.getAssignment(param);
return this.requisition.decrement(assignment).then(function() {
var arg = assignment.arg;
return arg == null ? undefined : arg.text;
});
});
}, {
request: {
typed: Arg(0, "string"), // The command string
param: Arg(1, "string") // The name of the parameter to parse
},
response: RetVal("string")
}),
/**
* Perform a lookup on a selection type to get the allowed values
*/
selectioninfo: method(function(commandName, paramName, action) {
var command = this.requisition.canon.getCommand(commandName);
if (command == null) {
throw new Error('No command called \'' + commandName + '\'');
}
var type;
command.params.forEach(function(param) {
if (param.name === paramName) {
type = param.type;
}
});
if (type == null) {
throw new Error('No parameter called \'' + paramName + '\' in \'' +
commandName + '\'');
}
switch (action) {
case 'lookup':
return type.lookup(this.requisition.executionContext);
case 'data':
return type.data(this.requisition.executionContext);
default:
throw new Error('Action must be either \'lookup\' or \'data\'');
}
}, {
request: {
commandName: Arg(0, "string"), // The command containing the parameter in question
paramName: Arg(1, "string"), // The name of the parameter
action: Arg(2, "string") // 'lookup' or 'data' depending on the function to call
},
response: RetVal("json")
}),
/**
* Execute a system command
* @return a promise of a string containing the output of the command
*/
system: method(function(cmd, args, cwd, env) {
return host.exec({ cmd: cmd, args: args, cwd: cwd, env: env });
}, {
request: {
cmd: Arg(0, "string"), // The executable to call
args: Arg(1, "array:string"), // Arguments to the executable
cwd: Arg(2, "string"), // The working directory
env: Arg(3, "json") // A map of environment variables
},
response: RetVal("json")
}),
/**
* Examine the filesystem for file matches
*/
parsefile: method(function(typed, filetype, existing, matches) {
var options = {
filetype: filetype,
existing: existing,
matches: new RegExp(matches)
};
return fileparser.parse(typed, options).then(function(reply) {
reply.status = reply.status.toString();
if (reply.predictor == null) {
return reply;
}
return reply.predictor().then(function(predictions) {
delete reply.predictor;
reply.predictions = predictions;
return reply;
});
});
}, {
request: {
typed: Arg(0, "string"), // The filename as typed by the user
filetype: Arg(1, "array:string"), // The expected filetype
existing: Arg(2, "string"), // Boolean which defines if a file/directory is expected to exist
matches: Arg(3, "json") // String of a regular expression which the result should match
},
response: RetVal("json")
})
};

View File

@ -1,58 +0,0 @@
/*
* 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';
/*
* GCLI is built from a number of components (called items) composed as
* required for each environment.
* When adding to or removing from this list, we should keep the basics in sync
* with the other environments.
* See:
* - lib/gcli/index.js: Generic basic set (without commands)
* - lib/gcli/demo.js: Adds demo commands to basic set for use in web demo
* - gcli.js: Add commands to basic set for use in Node command line
* - mozilla/gcli/index.js: From scratch listing for Firefox
* - lib/gcli/connectors/index.js: Client only items when executing remotely
* - lib/gcli/connectors/direct.js: Test items for connecting to in-process GCLI
*/
exports.items = [
require('./cli').items,
require('./commands/clear').items,
require('./commands/connect').items,
require('./commands/context').items,
require('./commands/exec').items,
require('./commands/global').items,
require('./commands/help').items,
require('./commands/intro').items,
require('./commands/lang').items,
require('./commands/mocks').items,
require('./commands/pref').items,
require('./commands/preflist').items,
require('./commands/test').items,
require('./commands/demo/alert').items,
require('./commands/demo/bugs').items,
require('./commands/demo/demo').items,
require('./commands/demo/echo').items,
require('./commands/demo/edit').items,
// require('./commands/demo/git').items,
// require('./commands/demo/hg').items,
require('./commands/demo/sleep').items,
require('./commands/demo/theme').items,
// Exclude Node commands on web
].reduce(function(prev, curr) { return prev.concat(curr); }, []);

View File

@ -16,12 +16,9 @@
'use strict';
var api = require('./api');
var Terminal = require('./ui/terminal').Terminal;
var settings = require('./settings');
// Patch-up old browsers
require('./util/legacy');
var Cc = require('chrome').Cc;
var Ci = require('chrome').Ci;
var Cu = require('chrome').Cu;
/*
* GCLI is built from a number of components (called items) composed as
@ -32,7 +29,7 @@ require('./util/legacy');
* - lib/gcli/index.js: Generic basic set (without commands)
* - lib/gcli/demo.js: Adds demo commands to basic set for use in web demo
* - gcli.js: Add commands to basic set for use in Node command line
* - mozilla/gcli/index.js: From scratch listing for Firefox
* - lib/gcli/index.js: (mozmaster branch) From scratch listing for Firefox
* - lib/gcli/connectors/index.js: Client only items when executing remotely
* - lib/gcli/connectors/direct.js: Test items for connecting to in-process GCLI
*/
@ -60,40 +57,107 @@ var items = [
require('./converters/converters').items,
require('./converters/basic').items,
require('./converters/html').items,
// require('./converters/html').items, // Prevent use of innerHTML
require('./converters/terminal').items,
require('./languages/command').items,
require('./languages/javascript').items,
// require('./connectors/direct').items, // Loopback for testing only
// require('./connectors/rdp').items, // Firefox remote debug protocol
require('./connectors/websocket').items,
require('./connectors/xhr').items,
// require('./connectors/direct').items, // No need for loopback testing
// require('./connectors/rdp').items, // Needs fixing
// require('./connectors/websocket').items, // Not from chrome
// require('./connectors/xhr').items, // Not from chrome
// require('./cli').items, // No need for '{' with web console
require('./commands/clear').items,
// require('./commands/connect').items, // We need to fix our RDP connector
require('./commands/context').items,
// require('./commands/exec').items, // No exec in Firefox yet
require('./commands/global').items,
require('./commands/help').items,
// require('./commands/intro').items, // No need for intro command
require('./commands/lang').items,
// require('./commands/mocks').items, // Only for testing
require('./commands/pref').items,
// require('./commands/preflist').items, // Too slow in Firefox
// require('./commands/test').items, // Only for testing
// No demo or node commands
// No commands in the basic set
].reduce(function(prev, curr) { return prev.concat(curr); }, []);
var api = require('./api');
api.populateApi(exports);
exports.addItems(items);
/**
* createTerminal() calls 'Terminal.create()' but returns an object which
* exposes a much restricted set of functions rather than all those exposed
* by Terminal.
* This allows for robust testing without exposing too many internals.
* @param options See Terminal.create() for a description of the available
* options.
*/
exports.createTerminal = function(options) {
options = options || {};
if (options.settings != null) {
settings.setDefaults(options.settings);
}
var host = require('./util/host');
return Terminal.create(options).then(function(terminal) {
options.terminal = terminal;
terminal.language.showIntro();
return terminal;
});
exports.useTarget = host.script.useTarget;
/**
* This code is internal and subject to change without notice.
* createDisplay() 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('./mozui/ffdisplay').FFDisplay;
return new FFDisplay(opts);
};
var prefSvc = Cc['@mozilla.org/preferences-service;1']
.getService(Ci.nsIPrefService);
var prefBranch = prefSvc.getBranch(null).QueryInterface(Ci.nsIPrefBranch2);
exports.hiddenByChromePref = function() {
return !prefBranch.prefHasUserValue('devtools.chrome.enabled');
};
try {
var Services = Cu.import('resource://gre/modules/Services.jsm', {}).Services;
var stringBundle = Services.strings.createBundle(
'chrome://browser/locale/devtools/gclicommands.properties');
/**
* Lookup a string in the GCLI string bundle
*/
exports.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
*/
exports.lookupFormat = function(name, swaps) {
try {
return stringBundle.formatStringFromName(name, swaps, swaps.length);
}
catch (ex) {
throw new Error('Failure in lookupFormat(\'' + name + '\')');
}
};
}
catch (ex) {
console.error('Using string fallbacks', ex);
exports.lookup = function(name) {
return name;
};
exports.lookupFormat = function(name, swaps) {
return name;
};
}

View File

@ -16,23 +16,32 @@
'use strict';
var imports = {};
var Cc = require('chrome').Cc;
var Ci = require('chrome').Ci;
var Cu = require('chrome').Cu;
var XPCOMUtils = Cu.import('resource://gre/modules/XPCOMUtils.jsm', {}).XPCOMUtils;
var Services = Cu.import('resource://gre/modules/Services.jsm', {}).Services;
XPCOMUtils.defineLazyGetter(imports, 'prefBranch', function() {
var prefService = Cc['@mozilla.org/preferences-service;1']
.getService(Ci.nsIPrefService);
return prefService.getBranch(null).QueryInterface(Ci.nsIPrefBranch2);
});
XPCOMUtils.defineLazyGetter(imports, 'supportsString', function() {
return Cc['@mozilla.org/supports-string;1']
.createInstance(Ci.nsISupportsString);
});
var util = require('./util/util');
/**
* Where we store the settings that we've created
* All local settings have this prefix when used in Firefox
*/
var settings = {};
/**
* Where the values for the settings are stored while in use.
*/
var settingValues = {};
/**
* Where the values for the settings are persisted for next use.
*/
var settingStorage;
var DEVTOOLS_PREFIX = 'devtools.gcli.';
/**
* The type library that we use in creating types for settings
@ -40,54 +49,216 @@ var settingStorage;
var types;
/**
* Allow a system to setup a different set of defaults from what GCLI provides
* A class to wrap up the properties of a preference.
* @see toolkit/components/viewconfig/content/config.js
*/
exports.setDefaults = function(newValues) {
Object.keys(newValues).forEach(function(name) {
if (settingValues[name] === undefined) {
settingValues[name] = newValues[name];
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.createType('boolean');
case imports.prefBranch.PREF_INT:
return types.createType('number');
case imports.prefBranch.PREF_STRING:
return types.createType('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,
Ci.nsISupportsString).data;
// In case of a localized string
if (/^chrome:\/\/.+\/locale\/.+\.properties/.test(value)) {
value = imports.prefBranch.getComplexValue(this.name,
Ci.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,
Ci.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);
};
/**
* Collection of preferences for sorted access
*/
var settingsAll = [];
/**
* Collection of preferences for fast indexed access
*/
var settingsMap = new Map();
/**
* Flag so we know if we've read the system preferences
*/
var hasReadSystem = false;
/**
* Clear out all preferences and return to initial state
*/
function reset() {
settingsMap = new Map();
settingsAll = [];
hasReadSystem = false;
}
/**
* Reset everything on startup and shutdown because we're doing lazy loading
*/
exports.startup = function(t) {
reset();
types = t;
if (types == null) {
throw new Error('no types');
}
};
exports.shutdown = function() {
reset();
};
/**
* Load system prefs if they've not been loaded already
* @return true
*/
function readSystem() {
if (hasReadSystem) {
return;
}
imports.prefBranch.getChildList('').forEach(function(name) {
var setting = new Setting(name);
settingsAll.push(setting);
settingsMap.set(name, setting);
});
settingsAll.sort(function(s1, s2) {
return s1.name.localeCompare(s2.name);
});
hasReadSystem = true;
}
/**
* Get an array containing all known Settings filtered to match the given
* filter (string) at any point in the name of the setting
*/
exports.getAll = function(filter) {
readSystem();
if (filter == null) {
return settingsAll;
}
return settingsAll.filter(function(setting) {
return setting.name.indexOf(filter) !== -1;
});
};
/**
* Initialize the settingValues store from localStorage
*/
exports.startup = function(t) {
types = t;
settingStorage = new LocalSettingStorage();
settingStorage.load(settingValues);
};
exports.shutdown = function() {
};
/**
* 'static' function to get an array containing all known Settings
*/
exports.getAll = function(filter) {
var all = [];
Object.keys(settings).forEach(function(name) {
if (filter == null || name.indexOf(filter) !== -1) {
all.push(settings[name]);
}
}.bind(this));
all.sort(function(s1, s2) {
return s1.name.localeCompare(s2.name);
}.bind(this));
return all;
};
/**
* Add a new setting
* @return The new Setting object
* Add a new setting.
*/
exports.addSetting = function(prefSpec) {
var type = types.createType(prefSpec.type);
var setting = new Setting(prefSpec.name, type, prefSpec.description,
prefSpec.defaultValue);
settings[setting.name] = setting;
var setting = new Setting(prefSpec);
if (settingsMap.has(setting.name)) {
// Once exists already, we're going to need to replace it in the array
for (var i = 0; i < settingsAll.length; i++) {
if (settingsAll[i].name === setting.name) {
settingsAll[i] = setting;
}
}
}
settingsMap.set(setting.name, setting);
exports.onChange({ added: setting.name });
return setting;
};
@ -101,16 +272,28 @@ exports.addSetting = function(prefSpec) {
* @return The found Setting object, or undefined if the setting was not found
*/
exports.getSetting = function(name) {
return settings[name];
};
// We might be able to give the answer without needing to read all system
// settings if this is an internal setting
var found = settingsMap.get(name);
if (!found) {
found = settingsMap.get(DEVTOOLS_PREFIX + name);
}
/**
* Remove a setting
*/
exports.removeSetting = function(nameOrSpec) {
var name = typeof nameOrSpec === 'string' ? nameOrSpec : nameOrSpec.name;
delete settings[name];
exports.onChange({ removed: name });
if (found) {
return found;
}
if (hasReadSystem) {
return undefined;
}
else {
readSystem();
found = settingsMap.get(name);
if (!found) {
found = settingsMap.get(DEVTOOLS_PREFIX + name);
}
return found;
}
};
/**
@ -119,70 +302,6 @@ exports.removeSetting = function(nameOrSpec) {
exports.onChange = util.createEvent('Settings.onChange');
/**
* Implement the load() and save() functions to write a JSON string blob to
* localStorage
* Remove a setting. A no-op in this case
*/
function LocalSettingStorage() {
}
LocalSettingStorage.prototype.load = function(values) {
if (typeof localStorage === 'undefined') {
return;
}
var gcliSettings = localStorage.getItem('gcli-settings');
if (gcliSettings != null) {
var parsed = JSON.parse(gcliSettings);
Object.keys(parsed).forEach(function(name) {
values[name] = parsed[name];
});
}
};
LocalSettingStorage.prototype.save = function(values) {
if (typeof localStorage !== 'undefined') {
var json = JSON.stringify(values);
localStorage.setItem('gcli-settings', json);
}
};
exports.LocalSettingStorage = LocalSettingStorage;
/**
* A class to wrap up the properties of a Setting.
* @see toolkit/components/viewconfig/content/config.js
*/
function Setting(name, type, description, defaultValue) {
this.name = name;
this.type = type;
this.description = description;
this._defaultValue = defaultValue;
this.onChange = util.createEvent('Setting.onChange');
this.setDefault();
}
/**
* Reset this setting to it's initial default value
*/
Setting.prototype.setDefault = function() {
this.value = this._defaultValue;
};
/**
* All settings 'value's are saved in the settingValues object
*/
Object.defineProperty(Setting.prototype, 'value', {
get: function() {
return settingValues[this.name];
},
set: function(value) {
settingValues[this.name] = value;
settingStorage.save(settingValues);
this.onChange({ setting: this, value: value });
},
enumerable: true
});
exports.removeSetting = function() { };

View File

@ -1,9 +1,11 @@
<div class="gcli-menu-template">
<div class="gcli-menu-option" aria-live="polite" foreach="item in ${items}"
onclick="${onItemClickInternal}" title="${item.manual}">
<div class="gcli-menu-name">${item.name}</div>
<div class="gcli-menu-desc">${item.description}</div>
</div>
<div class="gcli-menu-more" if="${hasMore}">${l10n.fieldMenuMore}</div>
<div>
<table class="gcli-menu-template" aria-live="polite">
<tr class="gcli-menu-option" foreach="item in ${items}"
onclick="${onItemClickInternal}" title="${item.manual}">
<td class="gcli-menu-name">${item.name}</td>
<td class="gcli-menu-desc">${item.description}</td>
</tr>
</table>
<div class="gcli-menu-more" if="${items.hasMore}">${l10n.fieldMenuMore}</div>
</div>

File diff suppressed because one or more lines are too long

View File

@ -1,32 +0,0 @@
<div>
<div save="${displayElement}" class="gcli-display">
<div save="${topElement}" class="gcli-in-top">
<input save="${inputElement}" class="gcli-in-input" type="text" autofocus="autofocus" spellcheck="false"/>
<div save="${completeElement}" class="gcli-in-complete" tabindex="-1" aria-live="polite">
<!-- Sub template used to show completion -->
<div>
<loop foreach="member in ${statusMarkup}">
<span class="${member.className}">${member.string}</span>
</loop>
<span class="gcli-in-ontab">${directTabText}</span>
<span class="gcli-in-todo" foreach="param in ${emptyParameters}">${param}</span>
<span class="gcli-in-ontab">${arrowTabText}</span>
<span class="gcli-in-closebrace theme-comment" if="${unclosedJs}">}</span>
</div>
</div>
<div save="${promptElement}" class="gcli-prompt theme-fg-color6"></div>
</div>
<div save="${panelElement}" class="gcli-panel">
<div save="${tooltipElement}" class="gcli-tooltip">
<!-- Sub template used for popup hints -->
<div class="gcli-tt" aria-live="polite">
<div save="${descriptionEle}" class="gcli-tt-description">${language.description}</div>
${field.element}
<div save="${errorEle}" class="gcli-tt-error theme-fg-color7">${language.message}</div>
<div class="gcli-tt-highlight"></div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,668 +0,0 @@
/*
* 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';
var promise = require('../util/promise');
var util = require('../util/util');
var domtemplate = require('../util/domtemplate');
var KeyEvent = require('../util/util').KeyEvent;
var host = require('../util/host');
var languages = require('../languages/languages');
var History = require('./history').History;
var FocusManager = require('./focus').FocusManager;
var RESOLVED = promise.resolve(undefined);
/**
* Shared promises for loading resource files
*/
var resourcesPromise;
/**
* Asynchronous construction. Use Terminal.create();
* @private
*/
function Terminal() {
throw new Error('Use Terminal.create().then(...) rather than new Terminal()');
}
/**
* A wrapper to take care of the functions concerning an input element
* @param components Object that links to other UI components. GCLI provided:
* - requisition
* - document
*/
Terminal.create = function(options) {
if (resourcesPromise == null) {
resourcesPromise = promise.all([
host.staticRequire(module, './terminal.css'),
host.staticRequire(module, './terminal.html')
]);
}
return resourcesPromise.then(function(resources) {
var terminal = Object.create(Terminal.prototype);
return terminal._init(options, resources[0], resources[1]);
});
};
/**
* Asynchronous construction. Use Terminal.create();
* @private
*/
Terminal.prototype._init = function(options, terminalCss, terminalHtml) {
this.document = options.document || document;
this.options = options;
this.focusManager = new FocusManager(this.document);
this.onInputChange = util.createEvent('Terminal.onInputChange');
// Configure the UI
this.rootElement = this.document.getElementById('gcli-root');
if (!this.rootElement) {
throw new Error('Missing element, id=gcli-root');
}
this.rootElement.terminal = this;
// terminal.html contains sub-templates which we detach for later processing
var template = util.toDom(this.document, terminalHtml);
// JSDom appears to have a broken parentElement, so this is a workaround
// this.tooltipTemplate = template.querySelector('.gcli-tt');
// this.tooltipTemplate.parentElement.removeChild(this.tooltipTemplate);
var tooltipParent = template.querySelector('.gcli-tooltip');
this.tooltipTemplate = tooltipParent.children[0];
tooltipParent.removeChild(this.tooltipTemplate);
// this.completerTemplate = template.querySelector('.gcli-in-complete > div');
// this.completerTemplate.parentElement.removeChild(this.completerTemplate);
var completerParent = template.querySelector('.gcli-in-complete');
this.completerTemplate = completerParent.children[0];
completerParent.removeChild(this.completerTemplate);
// We want the spans to line up without the spaces in the template
util.removeWhitespace(this.completerTemplate, true);
// Now we've detached the sub-templates, load what is left
// The following elements are stored into 'this' by this template process:
// displayElement, panelElement, tooltipElement,
// inputElement, completeElement, promptElement
domtemplate.template(template, this, { stack: 'terminal.html' });
while (template.hasChildNodes()) {
this.rootElement.appendChild(template.firstChild);
}
if (terminalCss != null) {
this.style = util.importCss(terminalCss, this.document, 'gcli-tooltip');
}
this.tooltipElement.classList.add('gcli-panel-hide');
// Firefox doesn't autofocus with dynamically added elements (Bug 662496)
this.inputElement.focus();
// Used to distinguish focus from TAB in CLI. See onKeyUp()
this.lastTabDownAt = 0;
// Setup History
this.history = new History();
this._scrollingThroughHistory = false;
// Initially an asynchronous completion isn't in-progress
this._completed = RESOLVED;
// Avoid updating when the keyUp results in no change
this._previousValue = undefined;
// We cache the fields we create so we can destroy them later
this.fields = [];
// Bind handlers
this.focus = this.focus.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
this.onMouseUp = this.onMouseUp.bind(this);
this.onOutput = this.onOutput.bind(this);
this.rootElement.addEventListener('click', this.focus, false);
// Ensure that TAB/UP/DOWN isn't handled by the browser
this.inputElement.addEventListener('keydown', this.onKeyDown, false);
this.inputElement.addEventListener('keyup', this.onKeyUp, false);
// Cursor position affects hint severity
this.inputElement.addEventListener('mouseup', this.onMouseUp, false);
this.focusManager.onVisibilityChange.add(this.visibilityChanged, this);
this.focusManager.addMonitoredElement(this.tooltipElement, 'tooltip');
this.focusManager.addMonitoredElement(this.inputElement, 'input');
this.onInputChange.add(this.updateCompletion, this);
host.script.onOutput.add(this.onOutput);
// Use the default language
return this.switchLanguage(null).then(function() {
return this;
}.bind(this));
};
/**
* Avoid memory leaks
*/
Terminal.prototype.destroy = function() {
this.focusManager.removeMonitoredElement(this.inputElement, 'input');
this.focusManager.removeMonitoredElement(this.tooltipElement, 'tooltip');
this.focusManager.onVisibilityChange.remove(this.visibilityChanged, this);
this.inputElement.removeEventListener('mouseup', this.onMouseUp, false);
this.inputElement.removeEventListener('keydown', this.onKeyDown, false);
this.inputElement.removeEventListener('keyup', this.onKeyUp, false);
this.rootElement.removeEventListener('click', this.focus, false);
this.language.destroy();
this.history.destroy();
this.focusManager.destroy();
if (this.style) {
this.style.parentNode.removeChild(this.style);
this.style = undefined;
}
this.field.onFieldChange.remove(this.fieldChanged, this);
this.field.destroy();
this.onInputChange.remove(this.updateCompletion, this);
// Remove the output elements so they free the event handers
util.clearElement(this.displayElement);
this.focus = undefined;
this.onMouseUp = undefined;
this.onKeyDown = undefined;
this.onKeyUp = undefined;
this.rootElement = undefined;
this.inputElement = undefined;
this.promptElement = undefined;
this.completeElement = undefined;
this.tooltipElement = undefined;
this.panelElement = undefined;
this.displayElement = undefined;
this.completerTemplate = undefined;
this.tooltipTemplate = undefined;
this.errorEle = undefined;
this.descriptionEle = undefined;
this.document = undefined;
};
/**
* Use an alternative language
*/
Terminal.prototype.switchLanguage = function(name) {
if (this.language != null) {
this.language.destroy();
}
return languages.createLanguage(name, this).then(function(language) {
this._updateLanguage(language);
}.bind(this));
};
/**
* Temporarily use an alternative language
*/
Terminal.prototype.pushLanguage = function(name) {
return languages.createLanguage(name, this).then(function(language) {
this.origLanguage = this.language;
this._updateLanguage(language);
}.bind(this));
};
/**
* Return to use the original language
*/
Terminal.prototype.popLanguage = function() {
if (this.origLanguage == null) {
return RESOLVED;
}
this._updateLanguage(this.origLanguage);
this.origLanguage = undefined;
return RESOLVED;
};
/**
* Internal helper to make sure everything knows about the new language
*/
Terminal.prototype._updateLanguage = function(language) {
this.language = language;
if (this.language.proportionalFonts) {
this.topElement.classList.remove('gcli-in-script');
}
else {
this.topElement.classList.add('gcli-in-script');
}
this.language.updateHints();
this.updateCompletion();
this.promptElement.innerHTML = this.language.prompt;
};
/**
* Sometimes the environment provides asynchronous output, we display it here
*/
Terminal.prototype.onOutput = function(ev) {
console.log('onOutput', ev);
var rowoutEle = this.document.createElement('pre');
rowoutEle.classList.add('gcli-row-out');
rowoutEle.classList.add('gcli-row-script');
rowoutEle.setAttribute('aria-live', 'assertive');
var output = ' // ';
if (ev.level === 'warn') {
output += '!';
}
else if (ev.level === 'error') {
output += '✖';
}
else {
output += '→';
}
output += ' ' + ev.arguments.map(function(arg) {
return arg;
}).join(',');
rowoutEle.innerHTML = output;
this.addElement(rowoutEle);
};
/**
* Handler for the input-element.onMouseUp event
*/
Terminal.prototype.onMouseUp = function(ev) {
this.language.caretMoved(this.inputElement.selectionStart);
};
/**
* Set the input field to a value, for external use.
* It does not execute the input or affect the history.
* This function should not be called internally, by Terminal and never as a
* result of a keyboard event on this.inputElement or bug 676520 could be
* triggered.
*/
Terminal.prototype.setInput = function(str) {
this._scrollingThroughHistory = false;
return this._setInput(str);
};
/**
* @private Internal version of setInput
*/
Terminal.prototype._setInput = function(str) {
this.inputElement.value = str;
this._previousValue = this.inputElement.value;
this._completed = this.language.handleInput(str);
return this._completed;
};
/**
* Focus the input element
*/
Terminal.prototype.focus = function() {
this.inputElement.focus();
this.language.caretMoved(this.inputElement.selectionStart);
};
/**
* Ensure certain keys (arrows, tab, etc) that we would like to handle
* are not handled by the browser
*/
Terminal.prototype.onKeyDown = function(ev) {
if (ev.keyCode === KeyEvent.DOM_VK_UP ||
ev.keyCode === KeyEvent.DOM_VK_DOWN) {
ev.preventDefault();
return;
}
// The following keys do not affect the state of the command line so we avoid
// informing the focusManager about keyboard events that involve these keys
if (ev.keyCode === KeyEvent.DOM_VK_F1 ||
ev.keyCode === KeyEvent.DOM_VK_ESCAPE ||
ev.keyCode === KeyEvent.DOM_VK_UP ||
ev.keyCode === KeyEvent.DOM_VK_DOWN) {
return;
}
if (this.focusManager) {
this.focusManager.onInputChange();
}
if (ev.keyCode === KeyEvent.DOM_VK_BACK_SPACE &&
this.inputElement.value === '') {
return this.popLanguage();
}
if (ev.keyCode === KeyEvent.DOM_VK_TAB) {
this.lastTabDownAt = 0;
if (!ev.shiftKey) {
ev.preventDefault();
// Record the timestamp of this TAB down so onKeyUp can distinguish
// focus from TAB in the CLI.
this.lastTabDownAt = ev.timeStamp;
}
if (ev.metaKey || ev.altKey || ev.crtlKey) {
if (this.document.commandDispatcher) {
this.document.commandDispatcher.advanceFocus();
}
else {
this.inputElement.blur();
}
}
}
};
/**
* Handler for use with DOM events, which just calls the promise enabled
* handleKeyUp function but checks the exit state of the promise so we know
* if something went wrong.
*/
Terminal.prototype.onKeyUp = function(ev) {
this.handleKeyUp(ev).then(null, util.errorHandler);
};
/**
* The main keyboard processing loop
* @return A promise that resolves (to undefined) when the actions kicked off
* by this handler are completed.
*/
Terminal.prototype.handleKeyUp = function(ev) {
if (this.focusManager && ev.keyCode === KeyEvent.DOM_VK_F1) {
this.focusManager.helpRequest();
return RESOLVED;
}
if (this.focusManager && ev.keyCode === KeyEvent.DOM_VK_ESCAPE) {
if (this.focusManager.isTooltipVisible ||
this.focusManager.isOutputVisible) {
this.focusManager.removeHelp();
}
else if (this.inputElement.value === '') {
return this.popLanguage();
}
return RESOLVED;
}
if (ev.keyCode === KeyEvent.DOM_VK_UP) {
if (this.isMenuShowing) {
return this.incrementChoice();
}
if (this.inputElement.value === '' || this._scrollingThroughHistory) {
this._scrollingThroughHistory = true;
return this._setInput(this.history.backward());
}
return this.language.handleUpArrow().then(function(handled) {
if (!handled) {
return this.incrementChoice();
}
}.bind(this));
}
if (ev.keyCode === KeyEvent.DOM_VK_DOWN) {
if (this.isMenuShowing) {
return this.decrementChoice();
}
if (this.inputElement.value === '' || this._scrollingThroughHistory) {
this._scrollingThroughHistory = true;
return this._setInput(this.history.forward());
}
return this.language.handleDownArrow().then(function(handled) {
if (!handled) {
return this.decrementChoice();
}
}.bind(this));
}
if (ev.keyCode === KeyEvent.DOM_VK_RETURN) {
var input = this.inputElement.value;
return this.language.handleReturn(input).then(function(handled) {
if (!handled) {
this._scrollingThroughHistory = false;
if (!this.selectChoice()) {
this.focusManager.setError(true);
}
else {
return this.popLanguage();
}
}
else {
return this.popLanguage();
}
}.bind(this));
}
if (ev.keyCode === KeyEvent.DOM_VK_TAB && !ev.shiftKey) {
// Being able to complete 'nothing' is OK if there is some context, but
// when there is nothing on the command line it just looks bizarre.
var hasContents = (this.inputElement.value.length > 0);
// If the TAB keypress took the cursor from another field to this one,
// then they get the keydown/keypress, and we get the keyup. In this
// case we don't want to do any completion.
// If the time of the keydown/keypress of TAB was close (i.e. within
// 1 second) to the time of the keyup then we assume that we got them
// both, and do the completion.
if (hasContents && this.lastTabDownAt + 1000 > ev.timeStamp) {
this._completed = this.language.handleTab();
}
else {
this._completed = RESOLVED;
}
this.lastTabDownAt = 0;
this._scrollingThroughHistory = false;
return this._completed;
}
if (this._previousValue === this.inputElement.value) {
return RESOLVED;
}
var value = this.inputElement.value;
this._scrollingThroughHistory = false;
this._previousValue = this.inputElement.value;
this._completed = this.language.handleInput(value);
return this._completed;
};
/**
* What is the index of the currently highlighted option?
*/
Terminal.prototype.getChoiceIndex = function() {
return this.field && this.field.menu ? this.field.menu.getChoiceIndex() : 0;
};
/**
* Don't show any menu options
*/
Terminal.prototype.unsetChoice = function() {
if (this.field && this.field.menu) {
this.field.menu.unsetChoice();
}
return this.updateCompletion();
};
/**
* Select the previous option in a list of choices
*/
Terminal.prototype.incrementChoice = function() {
if (this.field && this.field.menu) {
this.field.menu.incrementChoice();
}
return this.updateCompletion();
};
/**
* Select the next option in a list of choices
*/
Terminal.prototype.decrementChoice = function() {
if (this.field && this.field.menu) {
this.field.menu.decrementChoice();
}
return this.updateCompletion();
};
/**
* Pull together an input object, which may include XUL hacks
*/
Terminal.prototype.getInputState = function() {
var input = {
typed: this.inputElement.value,
cursor: {
start: this.inputElement.selectionStart,
end: this.inputElement.selectionEnd
}
};
// Workaround for potential XUL bug 676520 where textbox gives incorrect
// values for its content
if (input.typed == null) {
input = { typed: '', cursor: { start: 0, end: 0 } };
}
return input;
};
/**
* Bring the completion element up to date with what the language says
*/
Terminal.prototype.updateCompletion = function() {
return this.language.getCompleterTemplateData().then(function(data) {
var template = this.completerTemplate.cloneNode(true);
domtemplate.template(template, data, { stack: 'terminal.html#completer' });
util.clearElement(this.completeElement);
while (template.hasChildNodes()) {
this.completeElement.appendChild(template.firstChild);
}
}.bind(this));
};
/**
* The terminal acts on UP/DOWN if there is a menu showing
*/
Object.defineProperty(Terminal.prototype, 'isMenuShowing', {
get: function() {
return this.focusManager.isTooltipVisible &&
this.field != null &&
this.field.menu != null;
},
enumerable: true
});
/**
* Allow the terminal to use RETURN to chose the current menu item when
* it can't execute the command line
* @return true if there was a selection to use, false otherwise
*/
Terminal.prototype.selectChoice = function(ev) {
if (this.field && this.field.selectChoice) {
return this.field.selectChoice();
}
return false;
};
/**
* Called by the onFieldChange event on the current Field
*/
Terminal.prototype.fieldChanged = function(ev) {
this.language.fieldChanged(ev);
// Nasty hack, the terminal won't know about the text change yet, so it will
// get it's calculations wrong. We need to wait until the current set of
// changes has had a chance to propagate
this.document.defaultView.setTimeout(function() {
this.focus();
}.bind(this), 10);
};
/**
* Tweak CSS to show/hide the output
*/
Terminal.prototype.visibilityChanged = function(ev) {
if (!this.panelElement) {
return;
}
if (ev.tooltipVisible) {
this.tooltipElement.classList.remove('gcli-panel-hide');
}
else {
this.tooltipElement.classList.add('gcli-panel-hide');
}
this.scrollToBottom();
};
/**
* For language to add elements to the output
*/
Terminal.prototype.addElement = function(element) {
this.displayElement.insertBefore(element, this.topElement);
};
/**
* Clear the added added output
*/
Terminal.prototype.clear = function() {
while (this.displayElement.hasChildNodes()) {
if (this.displayElement.firstChild === this.topElement) {
break;
}
this.displayElement.removeChild(this.displayElement.firstChild);
}
};
/**
* Scroll the output area down to make the input visible
*/
Terminal.prototype.scrollToBottom = function() {
// We need to see the output of the latest command entered
// Certain browsers have a bug such that scrollHeight is too small
// when content does not fill the client area of the element
var scrollHeight = Math.max(this.displayElement.scrollHeight,
this.displayElement.clientHeight);
this.displayElement.scrollTop =
scrollHeight - this.displayElement.clientHeight;
};
exports.Terminal = Terminal;

View File

@ -14,589 +14,8 @@
* limitations under the License.
*/
/* jshint strict:false */
//
//
//
'use strict';
'do not use strict';
// WARNING: do not 'use strict' without reading the notes in envEval();
// Also don't remove the 'do not use strict' marker. The orion build uses these
// markers to know where to insert AMD headers.
/**
* For full documentation, see:
* https://github.com/mozilla/domtemplate/blob/master/README.md
*/
/**
* Begin a new templating process.
* @param node A DOM element or string referring to an element's id
* @param data Data to use in filling out the template
* @param options Options to customize the template processing. One of:
* - allowEval: boolean (default false) Basic template interpolations are
* either property paths (e.g. ${a.b.c.d}), or if allowEval=true then we
* allow arbitrary JavaScript
* - stack: string or array of strings (default empty array) The template
* engine maintains a stack of tasks to help debug where it is. This allows
* this stack to be prefixed with a template name
* - blankNullUndefined: By default DOMTemplate exports null and undefined
* values using the strings 'null' and 'undefined', which can be helpful for
* debugging, but can introduce unnecessary extra logic in a template to
* convert null/undefined to ''. By setting blankNullUndefined:true, this
* conversion is handled by DOMTemplate
*/
var template = function(node, data, options) {
var state = {
options: options || {},
// We keep a track of the nodes that we've passed through so we can keep
// data.__element pointing to the correct node
nodes: []
};
state.stack = state.options.stack;
if (!Array.isArray(state.stack)) {
if (typeof state.stack === 'string') {
state.stack = [ options.stack ];
}
else {
state.stack = [];
}
}
processNode(state, node, data);
};
if (typeof exports !== 'undefined') {
exports.template = template;
}
/**
* Helper for the places where we need to act asynchronously and keep track of
* where we are right now
*/
function cloneState(state) {
return {
options: state.options,
stack: state.stack.slice(),
nodes: state.nodes.slice()
};
}
/**
* Regex used to find ${...} sections in some text.
* Performance note: This regex uses ( and ) to capture the 'script' for
* further processing. Not all of the uses of this regex use this feature so
* if use of the capturing group is a performance drain then we should split
* this regex in two.
*/
var TEMPLATE_REGION = /\$\{([^}]*)\}/g;
/**
* Recursive function to walk the tree processing the attributes as it goes.
* @param node the node to process. If you pass a string in instead of a DOM
* element, it is assumed to be an id for use with document.getElementById()
* @param data the data to use for node processing.
*/
function processNode(state, node, data) {
if (typeof node === 'string') {
node = document.getElementById(node);
}
if (data == null) {
data = {};
}
state.stack.push(node.nodeName + (node.id ? '#' + node.id : ''));
var pushedNode = false;
try {
// Process attributes
if (node.attributes && node.attributes.length) {
// We need to handle 'foreach' and 'if' first because they might stop
// some types of processing from happening, and foreach must come first
// because it defines new data on which 'if' might depend.
if (node.hasAttribute('foreach')) {
processForEach(state, node, data);
return;
}
if (node.hasAttribute('if')) {
if (!processIf(state, node, data)) {
return;
}
}
// Only make the node available once we know it's not going away
state.nodes.push(data.__element);
data.__element = node;
pushedNode = true;
// It's good to clean up the attributes when we've processed them,
// but if we do it straight away, we mess up the array index
var attrs = Array.prototype.slice.call(node.attributes);
for (var i = 0; i < attrs.length; i++) {
var value = attrs[i].value;
var name = attrs[i].name;
state.stack.push(name);
try {
if (name === 'save') {
// Save attributes are a setter using the node
value = stripBraces(state, value);
property(state, value, data, node);
node.removeAttribute('save');
}
else if (name.substring(0, 2) === 'on') {
// If this attribute value contains only an expression
if (value.substring(0, 2) === '${' && value.slice(-1) === '}' &&
value.indexOf('${', 2) === -1) {
value = stripBraces(state, value);
var func = property(state, value, data);
if (typeof func === 'function') {
node.removeAttribute(name);
var capture = node.hasAttribute('capture' + name.substring(2));
node.addEventListener(name.substring(2), func, capture);
if (capture) {
node.removeAttribute('capture' + name.substring(2));
}
}
else {
// Attribute value is not a function - use as a DOM-L0 string
node.setAttribute(name, func);
}
}
else {
// Attribute value is not a single expression use as DOM-L0
node.setAttribute(name, processString(state, value, data));
}
}
else {
node.removeAttribute(name);
// Remove '_' prefix of attribute names so the DOM won't try
// to use them before we've processed the template
if (name.charAt(0) === '_') {
name = name.substring(1);
}
// Async attributes can only work if the whole attribute is async
var replacement;
if (value.indexOf('${') === 0 &&
value.charAt(value.length - 1) === '}') {
replacement = envEval(state, value.slice(2, -1), data, value);
if (replacement && typeof replacement.then === 'function') {
node.setAttribute(name, '');
/* jshint loopfunc:true */
replacement.then(function(newValue) {
node.setAttribute(name, newValue);
}).then(null, console.error);
}
else {
if (state.options.blankNullUndefined && replacement == null) {
replacement = '';
}
node.setAttribute(name, replacement);
}
}
else {
node.setAttribute(name, processString(state, value, data));
}
}
}
finally {
state.stack.pop();
}
}
}
// Loop through our children calling processNode. First clone them, so the
// set of nodes that we visit will be unaffected by additions or removals.
var childNodes = Array.prototype.slice.call(node.childNodes);
for (var j = 0; j < childNodes.length; j++) {
processNode(state, childNodes[j], data);
}
if (node.nodeType === 3 /*Node.TEXT_NODE*/) {
processTextNode(state, node, data);
}
}
finally {
if (pushedNode) {
data.__element = state.nodes.pop();
}
state.stack.pop();
}
}
/**
* Handle attribute values where the output can only be a string
*/
function processString(state, value, data) {
return value.replace(TEMPLATE_REGION, function(path) {
var insert = envEval(state, path.slice(2, -1), data, value);
return state.options.blankNullUndefined && insert == null ? '' : insert;
});
}
/**
* Handle <x if="${...}">
* @param node An element with an 'if' attribute
* @param data The data to use with envEval()
* @returns true if processing should continue, false otherwise
*/
function processIf(state, node, data) {
state.stack.push('if');
try {
var originalValue = node.getAttribute('if');
var value = stripBraces(state, originalValue);
var recurse = true;
try {
var reply = envEval(state, value, data, originalValue);
recurse = !!reply;
}
catch (ex) {
handleError(state, 'Error with \'' + value + '\'', ex);
recurse = false;
}
if (!recurse) {
node.parentNode.removeChild(node);
}
node.removeAttribute('if');
return recurse;
}
finally {
state.stack.pop();
}
}
/**
* Handle <x foreach="param in ${array}"> and the special case of
* <loop foreach="param in ${array}">.
* This function is responsible for extracting what it has to do from the
* attributes, and getting the data to work on (including resolving promises
* in getting the array). It delegates to processForEachLoop to actually
* unroll the data.
* @param node An element with a 'foreach' attribute
* @param data The data to use with envEval()
*/
function processForEach(state, node, data) {
state.stack.push('foreach');
try {
var originalValue = node.getAttribute('foreach');
var value = originalValue;
var paramName = 'param';
if (value.charAt(0) === '$') {
// No custom loop variable name. Use the default: 'param'
value = stripBraces(state, value);
}
else {
// Extract the loop variable name from 'NAME in ${ARRAY}'
var nameArr = value.split(' in ');
paramName = nameArr[0].trim();
value = stripBraces(state, nameArr[1].trim());
}
node.removeAttribute('foreach');
try {
var evaled = envEval(state, value, data, originalValue);
var cState = cloneState(state);
handleAsync(evaled, node, function(reply, siblingNode) {
processForEachLoop(cState, reply, node, siblingNode, data, paramName);
});
node.parentNode.removeChild(node);
}
catch (ex) {
handleError(state, 'Error with \'' + value + '\'', ex);
}
}
finally {
state.stack.pop();
}
}
/**
* Called by processForEach to handle looping over the data in a foreach loop.
* This works with both arrays and objects.
* Calls processForEachMember() for each member of 'set'
* @param set The object containing the data to loop over
* @param templNode The node to copy for each set member
* @param sibling The sibling node to which we add things
* @param data the data to use for node processing
* @param paramName foreach loops have a name for the parameter currently being
* processed. The default is 'param'. e.g. <loop foreach="param in ${x}">...
*/
function processForEachLoop(state, set, templNode, sibling, data, paramName) {
if (Array.isArray(set)) {
set.forEach(function(member, i) {
processForEachMember(state, member, templNode, sibling,
data, paramName, '' + i);
});
}
else {
for (var member in set) {
if (set.hasOwnProperty(member)) {
processForEachMember(state, member, templNode, sibling,
data, paramName, member);
}
}
}
}
/**
* Called by processForEachLoop() to resolve any promises in the array (the
* array itself can also be a promise, but that is resolved by
* processForEach()). Handle <LOOP> elements (which are taken out of the DOM),
* clone the template node, and pass the processing on to processNode().
* @param member The data item to use in templating
* @param templNode The node to copy for each set member
* @param siblingNode The parent node to which we add things
* @param data the data to use for node processing
* @param paramName The name given to 'member' by the foreach attribute
* @param frame A name to push on the stack for debugging
*/
function processForEachMember(state, member, templNode, siblingNode, data, paramName, frame) {
state.stack.push(frame);
try {
var cState = cloneState(state);
handleAsync(member, siblingNode, function(reply, node) {
data[paramName] = reply;
if (node.parentNode != null) {
var clone;
if (templNode.nodeName.toLowerCase() === 'loop') {
for (var i = 0; i < templNode.childNodes.length; i++) {
clone = templNode.childNodes[i].cloneNode(true);
node.parentNode.insertBefore(clone, node);
processNode(cState, clone, data);
}
}
else {
clone = templNode.cloneNode(true);
clone.removeAttribute('foreach');
node.parentNode.insertBefore(clone, node);
processNode(cState, clone, data);
}
}
delete data[paramName];
});
}
finally {
state.stack.pop();
}
}
/**
* Take a text node and replace it with another text node with the ${...}
* sections parsed out. We replace the node by altering node.parentNode but
* we could probably use a DOM Text API to achieve the same thing.
* @param node The Text node to work on
* @param data The data to use in calls to envEval()
*/
function processTextNode(state, node, data) {
// Replace references in other attributes
var value = node.data;
// We can't use the string.replace() with function trick (see generic
// attribute processing in processNode()) because we need to support
// functions that return DOM nodes, so we can't have the conversion to a
// string.
// Instead we process the string as an array of parts. In order to split
// the string up, we first replace '${' with '\uF001$' and '}' with '\uF002'
// We can then split using \uF001 or \uF002 to get an array of strings
// where scripts are prefixed with $.
// \uF001 and \uF002 are just unicode chars reserved for private use.
value = value.replace(TEMPLATE_REGION, '\uF001$$$1\uF002');
// Split a string using the unicode chars F001 and F002.
var parts = value.split(/\uF001|\uF002/);
if (parts.length > 1) {
parts.forEach(function(part) {
if (part === null || part === undefined || part === '') {
return;
}
if (part.charAt(0) === '$') {
part = envEval(state, part.slice(1), data, node.data);
}
var cState = cloneState(state);
handleAsync(part, node, function(reply, siblingNode) {
var doc = siblingNode.ownerDocument;
if (reply == null) {
reply = cState.options.blankNullUndefined ? '' : '' + reply;
}
if (typeof reply.cloneNode === 'function') {
// i.e. if (reply instanceof Element) { ...
reply = maybeImportNode(cState, reply, doc);
siblingNode.parentNode.insertBefore(reply, siblingNode);
}
else if (typeof reply.item === 'function' && reply.length) {
// NodeLists can be live, in which case maybeImportNode can
// remove them from the document, and thus the NodeList, which in
// turn breaks iteration. So first we clone the list
var list = Array.prototype.slice.call(reply, 0);
list.forEach(function(child) {
var imported = maybeImportNode(cState, child, doc);
siblingNode.parentNode.insertBefore(imported, siblingNode);
});
}
else {
// if thing isn't a DOM element then wrap its string value in one
reply = doc.createTextNode(reply.toString());
siblingNode.parentNode.insertBefore(reply, siblingNode);
}
});
});
node.parentNode.removeChild(node);
}
}
/**
* Return node or a import of node, if it's not in the given document
* @param node The node that we want to be properly owned
* @param doc The document that the given node should belong to
* @return A node that belongs to the given document
*/
function maybeImportNode(state, node, doc) {
return node.ownerDocument === doc ? node : doc.importNode(node, true);
}
/**
* A function to handle the fact that some nodes can be promises, so we check
* and resolve if needed using a marker node to keep our place before calling
* an inserter function.
* @param thing The object which could be real data or a promise of real data
* we use it directly if it's not a promise, or resolve it if it is.
* @param siblingNode The element before which we insert new elements.
* @param inserter The function to to the insertion. If thing is not a promise
* then handleAsync() is just 'inserter(thing, siblingNode)'
*/
function handleAsync(thing, siblingNode, inserter) {
if (thing != null && typeof thing.then === 'function') {
// Placeholder element to be replaced once we have the real data
var tempNode = siblingNode.ownerDocument.createElement('span');
siblingNode.parentNode.insertBefore(tempNode, siblingNode);
thing.then(function(delayed) {
inserter(delayed, tempNode);
if (tempNode.parentNode != null) {
tempNode.parentNode.removeChild(tempNode);
}
}).then(null, function(error) {
console.error(error.stack);
});
}
else {
inserter(thing, siblingNode);
}
}
/**
* Warn of string does not begin '${' and end '}'
* @param str the string to check.
* @return The string stripped of ${ and }, or untouched if it does not match
*/
function stripBraces(state, str) {
if (!str.match(TEMPLATE_REGION)) {
handleError(state, 'Expected ' + str + ' to match ${...}');
return str;
}
return str.slice(2, -1);
}
/**
* Combined getter and setter that works with a path through some data set.
* For example:
* <ul>
* <li>property(state, 'a.b', { a: { b: 99 }}); // returns 99
* <li>property(state, 'a', { a: { b: 99 }}); // returns { b: 99 }
* <li>property(state, 'a', { a: { b: 99 }}, 42); // returns 99 and alters the
* input data to be { a: { b: 42 }}
* </ul>
* @param path An array of strings indicating the path through the data, or
* a string to be cut into an array using <tt>split('.')</tt>
* @param data the data to use for node processing
* @param newValue (optional) If defined, this value will replace the
* original value for the data at the path specified.
* @return The value pointed to by <tt>path</tt> before any
* <tt>newValue</tt> is applied.
*/
function property(state, path, data, newValue) {
try {
if (typeof path === 'string') {
path = path.split('.');
}
var value = data[path[0]];
if (path.length === 1) {
if (newValue !== undefined) {
data[path[0]] = newValue;
}
if (typeof value === 'function') {
return value.bind(data);
}
return value;
}
if (!value) {
handleError(state, '"' + path[0] + '" is undefined');
return null;
}
return property(state, path.slice(1), value, newValue);
}
catch (ex) {
handleError(state, 'Path error with \'' + path + '\'', ex);
return '${' + path + '}';
}
}
/**
* Like eval, but that creates a context of the variables in <tt>env</tt> in
* which the script is evaluated.
* WARNING: This script uses 'with' which is generally regarded to be evil.
* The alternative is to create a Function at runtime that takes X parameters
* according to the X keys in the env object, and then call that function using
* the values in the env object. This is likely to be slow, but workable.
* @param script The string to be evaluated.
* @param data The environment in which to eval the script.
* @param frame Optional debugging string in case of failure.
* @return The return value of the script, or the error message if the script
* execution failed.
*/
function envEval(state, script, data, frame) {
try {
state.stack.push(frame.replace(/\s+/g, ' '));
// Detect if a script is capable of being interpreted using property()
if (/^[_a-zA-Z0-9.]*$/.test(script)) {
return property(state, script, data);
}
else {
if (!state.options.allowEval) {
handleError(state, 'allowEval is not set, however \'' + script + '\'' +
' can not be resolved using a simple property path.');
return '${' + script + '}';
}
/* jshint -W085 */
with (data) {
return eval(script);
}
}
}
catch (ex) {
handleError(state, 'Template error evaluating \'' + script + '\'', ex);
return '${' + script + '}';
}
finally {
state.stack.pop();
}
}
/**
* A generic way of reporting errors, for easy overloading in different
* environments.
* @param message the error message to report.
* @param ex optional associated exception.
*/
function handleError(state, message, ex) {
logError(message + ' (In: ' + state.stack.join(' > ') + ')');
if (ex) {
logError(ex);
}
}
/**
* A generic way of reporting errors, for easy overloading in different
* environments.
* @param message the error message to report.
*/
function logError(message) {
console.error(message);
}
var Cu = require('chrome').Cu;
var template = Cu.import('resource://gre/modules/devtools/Templater.jsm', {}).template;
exports.template = template;

View File

@ -16,36 +16,41 @@
'use strict';
var fs = require('fs');
var path = require('path');
var Cu = require('chrome').Cu;
var Cc = require('chrome').Cc;
var Ci = require('chrome').Ci;
var OS = Cu.import('resource://gre/modules/osfile.jsm', {}).OS;
var promise = require('./promise');
var util = require('./util');
/**
* A set of functions defining a filesystem API for fileparser.js
* A set of functions that don't really belong in 'fs' (because they're not
* really universal in scope) but also kind of do (because they're not specific
* to GCLI
*/
exports.join = path.join.bind(path);
exports.dirname = path.dirname.bind(path);
exports.sep = path.sep;
exports.home = process.env.HOME;
exports.join = OS.Path.join;
exports.sep = OS.Path.sep;
exports.dirname = OS.Path.dirname;
var dirService = Cc['@mozilla.org/file/directory_service;1']
.getService(Ci.nsIProperties);
exports.home = dirService.get('Home', Ci.nsIFile).path;
if ('winGetDrive' in OS.Path) {
exports.sep = '\\';
}
else {
exports.sep = '/';
}
/**
* The NodeJS docs suggest using ``pathname.split(path.sep)`` to cut a path
* into a number of components. But this doesn't take into account things like
* path normalization and removing the initial (or trailing) blanks from
* absolute (or / terminated) paths.
* http://www.nodejs.org/api/path.html#path_path_sep
* Split a path into its components.
* @param pathname (string) The part to cut up
* @return An array of path components
*/
exports.split = function(pathname) {
pathname = path.normalize(pathname);
var parts = pathname.split(exports.sep);
return parts.filter(function(part) {
return part !== '';
});
return OS.Path.split(pathname).components;
};
/**
@ -59,33 +64,28 @@ exports.split = function(pathname) {
* - filename: The final filename part of the pathname
*/
exports.ls = function(pathname, matches) {
var deferred = promise.defer();
var iterator = new OS.File.DirectoryIterator(pathname);
var entries = [];
fs.readdir(pathname, function(err, files) {
if (err) {
deferred.reject(err);
}
else {
if (matches) {
files = files.filter(function(file) {
return matches.test(file);
});
}
var statsPromise = util.promiseEach(files, function(filename) {
var filepath = path.join(pathname, filename);
return exports.stat(filepath).then(function(stats) {
stats.filename = filename;
stats.pathname = filepath;
return stats;
});
});
statsPromise.then(deferred.resolve, deferred.reject);
}
var iteratePromise = iterator.forEach(function(entry) {
entries.push({
exists: true,
isDir: entry.isDir,
isFile: !entry.isFile,
filename: entry.name,
pathname: entry.path
});
});
return deferred.promise;
return iteratePromise.then(function onSuccess() {
iterator.close();
return entries;
},
function onFailure(reason) {
iterator.close();
throw reason;
}
);
};
/**
@ -95,31 +95,26 @@ exports.ls = function(pathname, matches) {
* exists:true to stat blocks from existing paths
*/
exports.stat = function(pathname) {
var deferred = promise.defer();
var onResolve = function(stats) {
return {
exists: true,
isDir: stats.isDir,
isFile: !stats.isFile
};
};
fs.stat(pathname, function(err, stats) {
if (err) {
if (err.code === 'ENOENT') {
deferred.resolve({
exists: false,
isDir: false,
isFile: false
});
}
else {
deferred.reject(err);
}
var onReject = function(err) {
if (err instanceof OS.File.Error && err.becauseNoSuchFile) {
return {
exists: false,
isDir: false,
isFile: false
};
}
else {
deferred.resolve({
exists: true,
isDir: stats.isDirectory(),
isFile: stats.isFile()
});
}
});
throw err;
};
return deferred.promise;
return OS.File.stat(pathname).then(onResolve, onReject);
};
/**

View File

@ -16,19 +16,15 @@
'use strict';
// Warning - gcli.js causes this version of host.js to be favored in NodeJS
// which means that it's also used in testing in NodeJS
var Cu = require('chrome').Cu;
var Cc = require('chrome').Cc;
var Ci = require('chrome').Ci;
var childProcess = require('child_process');
var fs = require('fs');
var path = require('path');
var OS = Cu.import('resource://gre/modules/osfile.jsm', {}).OS;
var promise = require('./promise');
var util = require('./util');
var ATTR_NAME = '__gcli_border';
var HIGHLIGHT_STYLE = '1px dashed black';
function Highlighter(document) {
this._document = document;
this._nodes = util.createEmptyNodeList(this._document);
@ -53,19 +49,11 @@ Highlighter.prototype.destroy = function() {
};
Highlighter.prototype._highlightNode = function(node) {
if (node.hasAttribute(ATTR_NAME)) {
return;
}
var styles = this._document.defaultView.getComputedStyle(node);
node.setAttribute(ATTR_NAME, styles.border);
node.style.border = HIGHLIGHT_STYLE;
// Enable when the highlighter rewrite is done
};
Highlighter.prototype._unhighlightNode = function(node) {
var previous = node.getAttribute(ATTR_NAME);
node.style.border = previous;
node.removeAttribute(ATTR_NAME);
// Enable when the highlighter rewrite is done
};
exports.Highlighter = Highlighter;
@ -74,56 +62,43 @@ exports.Highlighter = Highlighter;
* See docs in lib/gcli/util/host.js:exec
*/
exports.exec = function(execSpec) {
var deferred = promise.defer();
var output = { data: '' };
var child = childProcess.spawn(execSpec.cmd, execSpec.args, {
env: execSpec.env,
cwd: execSpec.cwd
});
child.stdout.on('data', function(data) {
output.data += data;
});
child.stderr.on('data', function(data) {
output.data += data;
});
child.on('close', function(code) {
output.code = code;
if (code === 0) {
deferred.resolve(output);
}
else {
deferred.reject(output);
}
});
return deferred.promise;
throw new Error('Not supported');
};
/**
* Asynchronously load a text resource
* @param requistingModule Typically just 'module' to pick up the 'module'
* variable from the calling modules scope
* @param name The name of the resource to load, as a path (including extension)
* relative to that of the requiring module
* @return A promise of the contents of the file as a string
* @see lib/gcli/util/host.js
*/
exports.staticRequire = function(requistingModule, name) {
var deferred = promise.defer();
var parent = path.dirname(requistingModule.id);
var filename = parent + '/' + name;
if (name.match(/\.css$/)) {
deferred.resolve('');
}
else {
var filename = OS.Path.dirname(requistingModule.id) + '/' + name;
filename = filename.replace(/\/\.\//g, '/');
filename = 'resource://gre/modules/devtools/' + filename;
fs.readFile(filename, { encoding: 'utf8' }, function(err, data) {
if (err) {
var xhr = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
.createInstance(Ci.nsIXMLHttpRequest);
xhr.onload = function onload() {
deferred.resolve(xhr.responseText);
}.bind(this);
xhr.onabort = xhr.onerror = xhr.ontimeout = function(err) {
deferred.reject(err);
}
}.bind(this);
deferred.resolve(data);
});
try {
xhr.open('GET', filename);
xhr.send();
}
catch (ex) {
deferred.reject(ex);
}
}
return deferred.promise;
};
@ -132,27 +107,103 @@ exports.staticRequire = function(requistingModule, name) {
* A group of functions to help scripting. Small enough that it doesn't need
* a separate module (it's basically a wrapper around 'eval' in some contexts)
*/
exports.script = {
onOutput: util.createEvent('Script.onOutput'),
var client;
var target;
var consoleActor;
var webConsoleClient;
// Setup the environment to eval JavaScript, a no-op on the web
useTarget: function(tgt) { },
exports.script = { };
// Execute some JavaScript
eval: function(javascript) {
try {
return promise.resolve({
input: javascript,
output: eval(javascript),
exception: null
});
}
catch (ex) {
return promise.resolve({
input: javascript,
output: null,
exception: ex
});
}
}
exports.script.onOutput = util.createEvent('Script.onOutput');
/**
* Setup the environment to eval JavaScript
*/
exports.script.useTarget = function(tgt) {
target = tgt;
// Local debugging needs to make the target remote.
var targetPromise = target.isRemote ?
promise.resolve(target) :
target.makeRemote();
return targetPromise.then(function() {
var deferred = promise.defer();
client = target._client;
client.addListener('pageError', function(packet) {
if (packet.from === consoleActor) {
// console.log('pageError', packet.pageError);
exports.script.onOutput({
level: 'exception',
message: packet.exception.class
});
}
});
client.addListener('consoleAPICall', function(type, packet) {
if (packet.from === consoleActor) {
var data = packet.message;
var ev = {
level: data.level,
arguments: data.arguments,
};
if (data.filename !== 'debugger eval code') {
ev.source = {
filename: data.filename,
lineNumber: data.lineNumber,
functionName: data.functionName
};
}
exports.script.onOutput(ev);
}
});
consoleActor = target._form.consoleActor;
var onAttach = function(response, wcc) {
webConsoleClient = wcc;
if (response.error != null) {
deferred.reject(response);
}
else {
deferred.resolve(response);
}
// TODO: add _onTabNavigated code?
};
var listeners = [ 'PageError', 'ConsoleAPI' ];
client.attachConsole(consoleActor, listeners, onAttach);
return deferred.promise;
});
};
/**
* Execute some JavaScript
*/
exports.script.eval = function(javascript) {
var deferred = promise.defer();
var onResult = function(response) {
var output = response.result;
if (typeof output === 'object' && output.type === 'undefined') {
output = undefined;
}
deferred.resolve({
input: response.input,
output: output,
exception: response.exception
});
};
webConsoleClient.evaluateJS(javascript, onResult, {});
return deferred.promise;
};

View File

@ -16,663 +16,74 @@
'use strict';
var strings = {};
var Cu = require('chrome').Cu;
/**
* Add a CommonJS module to the list of places in which we look for
* localizations. Before calling this function, it's important to make a call
* to require(modulePath) to ensure that the dependency system (either require
* or dryice) knows to make the module ready.
* @param modulePath A CommonJS module (as used in calls to require). Don't
* add the 'i18n!' prefix used by requirejs.
* @see unregisterStringsSource()
var XPCOMUtils = Cu.import('resource://gre/modules/XPCOMUtils.jsm', {}).XPCOMUtils;
var Services = Cu.import('resource://gre/modules/Services.jsm', {}).Services;
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) {
// Bug 683844: Should be require('i18n!' + module);
var additions = require(modulePath).root;
Object.keys(additions).forEach(function(key) {
if (strings[key]) {
console.error('Key \'' + key + '\' (loaded from ' + modulePath + ') ' +
'already exists. Ignoring.');
return;
}
strings[key] = additions[key];
}, this);
throw new Error('registerStringsSource is not available in mozilla');
};
/**
* The main GCLI strings source is always required.
* We have to load it early on in the process (in the require phase) so that
* we can define settingSpecs and commandSpecs at the top level too.
*/
require('../nls/strings');
exports.registerStringsSource('../nls/strings');
/**
* Undo the effects of registerStringsSource().
* @param modulePath A CommonJS module (as used in calls to require).
* @see registerStringsSource()
*/
exports.unregisterStringsSource = function(modulePath) {
// Bug 683844: Should be require('i18n!' + module);
var additions = require(modulePath).root;
Object.keys(additions).forEach(function(key) {
delete strings[key];
}, this);
throw new Error('unregisterStringsSource is not available in mozilla');
};
/**
* Finds the preferred locales of the user as an array of RFC 4646 strings
* (e.g. 'pt-br').
* . There is considerable confusion as to the correct value
* since there are a number of places the information can be stored:
* - In the OS (IE:navigator.userLanguage, IE:navigator.systemLanguage)
* - In the browser (navigator.language, IE:navigator.browserLanguage)
* - By GEO-IP
* - By website specific settings
* This implementation uses navigator.language || navigator.userLanguage as
* this is compatible with requirejs.
* See http://tools.ietf.org/html/rfc4646
* See http://stackoverflow.com/questions/1043339/javascript-for-detecting-browser-language-preference
* See http://msdn.microsoft.com/en-us/library/ms534713.aspx
* @return The current locale as an RFC 4646 string
*/
exports.getPreferredLocales = function() {
var language = typeof navigator !== 'undefined' ?
(navigator.language || navigator.userLanguage).toLowerCase() :
'en-us';
var parts = language.split('-');
var reply = parts.map(function(part, index) {
return parts.slice(0, parts.length - index).join('-');
});
reply.push('root');
return reply;
};
/**
* Lookup a key in our strings file using localized versions if possible,
* throwing an error if that string does not exist.
* @param key The string to lookup
* This should generally be in the general form 'filenameExportIssue' where
* filename is the name of the module (all lowercase without underscores) and
* export is the name of a top level thing in which the message is used and
* issue is a short string indicating the issue.
* The point of a 'standard' like this is to keep strings fairly short whilst
* still allowing users to have an idea where they come from, and preventing
* name clashes.
* @return The string resolved from the correct locale
*/
exports.lookup = function(key) {
var str = strings[key];
if (str == null) {
throw new Error('No i18n key: ' + key);
}
return str;
};
/**
* An alternative to lookup().
* <tt>l10n.lookup('x') === l10n.propertyLookup.x</tt>
* We should go easy on this method until we are sure that we don't have too
* many 'old-browser' problems. However this works particularly well with the
* templater because you can pass this in to a template that does not do
* <tt>{ allowEval: true }</tt>
*/
if (typeof Proxy !== 'undefined') {
exports.propertyLookup = Proxy.create({
get: function(rcvr, name) {
return exports.lookup(name);
}
});
}
else {
exports.propertyLookup = strings;
}
/**
* Helper function to process swaps.
* For example:
* swap('the {subject} {verb} {preposition} the {object}', {
* subject: 'cat', verb: 'sat', preposition: 'on', object: 'mat'
* });
* Returns 'the cat sat on the mat'.
* @param str The string containing parts delimited by { and } to be replaced
* @param swaps Lookup map containing the replacement strings
*/
function swap(str, swaps) {
return str.replace(/\{[^}]*\}/g, function(name) {
name = name.slice(1, -1);
if (swaps == null) {
console.log('Missing swaps while looking up \'' + name + '\'');
return '';
}
var replacement = swaps[name];
if (replacement == null) {
console.log('Can\'t find \'' + name + '\' in ' + JSON.stringify(swaps));
replacement = '';
}
return replacement;
});
}
/**
* Lookup a key in our strings file using localized versions if possible,
* and perform string interpolation to inject runtime values into the string.
* l10n lookup is required for user visible strings, but not required for
* console messages and throw strings.
* lookupSwap() is virtually identical in function to lookupFormat(), except
* that lookupSwap() is easier to use, however lookupFormat() is required if
* your code is to work with Mozilla's i10n system.
* @param key The string to lookup
* This should generally be in the general form 'filename_export_issue' where
* filename is the name of the module (all lowercase without underscores) and
* export is the name of a top level thing in which the message is used and
* issue is a short string indicating the issue.
* The point of a 'standard' like this is to keep strings fairly short whilst
* still allowing users to have an idea where they come from, and preventing
* name clashes.
* The value looked up may contain {variables} to be exchanged using swaps
* @param swaps A map of variable values to be swapped.
* @return A looked-up and interpolated message for display to the user.
* @see lookupFormat()
*/
exports.lookupSwap = function(key, swaps) {
var str = exports.lookup(key);
return swap(str, swaps);
throw new Error('lookupSwap is not available in mozilla');
};
/**
* Perform the string swapping required by format().
* @see format() for details of the swaps performed.
*/
function format(str, swaps) {
// First replace the %S strings
var index = 0;
str = str.replace(/%S/g, function() {
return swaps[index++];
});
// Then %n$S style strings
str = str.replace(/%([0-9])\$S/g, function(match, idx) {
return swaps[idx - 1];
});
return str;
}
/**
* Lookup a key in our strings file using localized versions if possible,
* and perform string interpolation to inject runtime values into the string.
* l10n lookup is required for user visible strings, but not required for
* console messages and throw strings.
* lookupFormat() is virtually identical in function to lookupSwap(), except
* that lookupFormat() works with strings held in the mozilla repo in addition
* to files held outside.
* @param key Looks up the format string for the given key in the string bundle
* and returns a formatted copy where each occurrence of %S (uppercase) is
* replaced by each successive element in the supplied array.
* Alternatively, numbered indices of the format %n$S (e.g. %1$S, %2$S, etc.)
* can be used to specify the position of the corresponding parameter
* explicitly.
* The mozilla version performs more advances formatting than these simple
* cases, however these cases are not supported so far, mostly because they are
* not well documented.
* @param swaps An array of strings to be swapped.
* @return A looked-up and interpolated message for display to the user.
* @see https://developer.mozilla.org/en/XUL/Method/getFormattedString
*/
exports.lookupFormat = function(key, swaps) {
var str = exports.lookup(key);
return format(str, swaps);
};
/**
* Lookup the correct pluralization of a word/string.
* The first ``key`` and ``swaps`` parameters of lookupPlural() are the same
* as for lookupSwap(), however there is an extra ``ord`` parameter which indicates
* the plural ordinal to use.
* For example, in looking up the string '39 steps', the ordinal would be 39.
*
* More detailed example:
* French has 2 plural forms: the first for 0 and 1, the second for everything
* else. English also has 2, but the first only covers 1. Zero is lumped into
* the 'everything else' category. Vietnamese has only 1 plural form - so it
* uses the same noun form however many of them there are.
* The following localization strings describe how to pluralize the phrase
* '1 minute':
* 'en-us': { demo_plural_time: [ '{ord} minute', '{ord} minutes' ] },
* 'fr-fr': { demo_plural_time: [ '{ord} minute', '{ord} minutes' ] },
* 'vi-vn': { demo_plural_time: [ '{ord} phut' ] },
*
* l10n.lookupPlural('demo_plural_time', 0); // '0 minutes' in 'en-us'
* l10n.lookupPlural('demo_plural_time', 1); // '1 minute' in 'en-us'
* l10n.lookupPlural('demo_plural_time', 9); // '9 minutes' in 'en-us'
*
* l10n.lookupPlural('demo_plural_time', 0); // '0 minute' in 'fr-fr'
* l10n.lookupPlural('demo_plural_time', 1); // '1 minute' in 'fr-fr'
* l10n.lookupPlural('demo_plural_time', 9); // '9 minutes' in 'fr-fr'
*
* l10n.lookupPlural('demo_plural_time', 0); // '0 phut' in 'vi-vn'
* l10n.lookupPlural('demo_plural_time', 1); // '1 phut' in 'vi-vn'
* l10n.lookupPlural('demo_plural_time', 9); // '9 phut' in 'vi-vn'
*
* The
* Note that the localization strings are (correctly) the same (since both
* the English and the French words have the same etymology)
* @param key The string to lookup in gcli/nls/strings.js
* @param ord The number to use in plural lookup
* @param swaps A map of variable values to be swapped.
*/
exports.lookupPlural = function(key, ord, swaps) {
var index = getPluralRule().get(ord);
var words = exports.lookup(key);
var str = words[index];
swaps = swaps || {};
swaps.ord = ord;
return swap(str, swaps);
throw new Error('lookupPlural is not available in mozilla');
};
/**
* Find the correct plural rule for the current locale
* @return a plural rule with a 'get()' function
*/
function getPluralRule() {
if (!pluralRule) {
var lang = navigator.language || navigator.userLanguage;
// Convert lang to a rule index
pluralRules.some(function(rule) {
if (rule.locales.indexOf(lang) !== -1) {
pluralRule = rule;
return true;
}
return false;
});
exports.getPreferredLocales = function() {
return [ 'root' ];
};
// Use rule 0 by default, which is no plural forms at all
if (!pluralRule) {
console.error('Failed to find plural rule for ' + lang);
pluralRule = pluralRules[0];
/** @see lookup() in lib/gcli/util/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);
}
return pluralRule;
}
/**
* A plural form is a way to pluralize a noun. There are 2 simple plural forms
* in English, with (s) and without - e.g. tree and trees. There are many other
* ways to pluralize (e.g. witches, ladies, teeth, oxen, axes, data, alumini)
* However they all follow the rule that 1 is 'singular' while everything
* else is 'plural' (words without a plural form like sheep can be seen as
* following this rule where the singular and plural forms are the same)
* <p>Non-English languages have different pluralization rules, for example
* French uses singular for 0 as well as 1. Japanese has no plurals while
* Arabic and Russian are very complex.
*
* See https://developer.mozilla.org/en/Localization_and_Plurals
* See https://secure.wikimedia.org/wikipedia/en/wiki/List_of_ISO_639-1_codes
*
* Contains code inspired by Mozilla L10n code originally developed by
* Edward Lee <edward.lee@engineering.uiuc.edu>
*/
var pluralRules = [
/**
* Index 0 - Only one form for all
* Asian family: Japanese, Vietnamese, Korean
*/
{
locales: [
'fa', 'fa-ir',
'id',
'ja', 'ja-jp-mac',
'ka',
'ko', 'ko-kr',
'th', 'th-th',
'tr', 'tr-tr',
'zh', 'zh-tw', 'zh-cn'
],
numForms: 1,
get: function(n) {
return 0;
}
},
/**
* Index 1 - Two forms, singular used for one only
* Germanic family: English, German, Dutch, Swedish, Danish, Norwegian,
* Faroese
* Romanic family: Spanish, Portuguese, Italian, Bulgarian
* Latin/Greek family: Greek
* Finno-Ugric family: Finnish, Estonian
* Semitic family: Hebrew
* Artificial: Esperanto
* Finno-Ugric family: Hungarian
* Turkic/Altaic family: Turkish
*/
{
locales: [
'af', 'af-za',
'as', 'ast',
'bg',
'br',
'bs', 'bs-ba',
'ca',
'cy', 'cy-gb',
'da',
'de', 'de-de', 'de-ch',
'en', 'en-gb', 'en-us', 'en-za',
'el', 'el-gr',
'eo',
'es', 'es-es', 'es-ar', 'es-cl', 'es-mx',
'et', 'et-ee',
'eu',
'fi', 'fi-fi',
'fy', 'fy-nl',
'gl', 'gl-gl',
'he',
// 'hi-in', Without an unqualified language, looks dodgy
'hu', 'hu-hu',
'hy', 'hy-am',
'it', 'it-it',
'kk',
'ku',
'lg',
'mai',
// 'mk', 'mk-mk', Should be 14?
'ml', 'ml-in',
'mn',
'nb', 'nb-no',
'no', 'no-no',
'nl',
'nn', 'nn-no',
'no', 'no-no',
'nb', 'nb-no',
'nso', 'nso-za',
'pa', 'pa-in',
'pt', 'pt-pt',
'rm', 'rm-ch',
// 'ro', 'ro-ro', Should be 5?
'si', 'si-lk',
// 'sl', Should be 10?
'son', 'son-ml',
'sq', 'sq-al',
'sv', 'sv-se',
'vi', 'vi-vn',
'zu', 'zu-za'
],
numForms: 2,
get: function(n) {
return n != 1 ?
1 :
0;
}
},
/**
* Index 2 - Two forms, singular used for zero and one
* Romanic family: Brazilian Portuguese, French
*/
{
locales: [
'ak', 'ak-gh',
'bn', 'bn-in', 'bn-bd',
'fr', 'fr-fr',
'gu', 'gu-in',
'kn', 'kn-in',
'mr', 'mr-in',
'oc', 'oc-oc',
'or', 'or-in',
'pt-br',
'ta', 'ta-in', 'ta-lk',
'te', 'te-in'
],
numForms: 2,
get: function(n) {
return n > 1 ?
1 :
0;
}
},
/**
* Index 3 - Three forms, special case for zero
* Latvian
*/
{
locales: [ 'lv' ],
numForms: 3,
get: function(n) {
return n % 10 == 1 && n % 100 != 11 ?
1 :
n !== 0 ?
2 :
0;
}
},
/**
* Index 4 -
* Scottish Gaelic
*/
{
locales: [ 'gd', 'gd-gb' ],
numForms: 4,
get: function(n) {
return n == 1 || n == 11 ?
0 :
n == 2 || n == 12 ?
1 :
n > 0 && n < 20 ?
2 :
3;
}
},
/**
* Index 5 - Three forms, special case for numbers ending in 00 or [2-9][0-9]
* Romanian
*/
{
locales: [ 'ro', 'ro-ro' ],
numForms: 3,
get: function(n) {
return n == 1 ?
0 :
n === 0 || n % 100 > 0 && n % 100 < 20 ?
1 :
2;
}
},
/**
* Index 6 - Three forms, special case for numbers ending in 1[2-9]
* Lithuanian
*/
{
locales: [ 'lt' ],
numForms: 3,
get: function(n) {
return n % 10 == 1 && n % 100 != 11 ?
0 :
n % 10 >= 2 && (n % 100 < 10 || n % 100 >= 20) ?
2 :
1;
}
},
/**
* Index 7 - Three forms, special cases for numbers ending in 1 and
* 2, 3, 4, except those ending in 1[1-4]
* Slavic family: Russian, Ukrainian, Serbian, Croatian
*/
{
locales: [
'be', 'be-by',
'hr', 'hr-hr',
'ru', 'ru-ru',
'sr', 'sr-rs', 'sr-cs',
'uk'
],
numForms: 3,
get: function(n) {
return n % 10 == 1 && n % 100 != 11 ?
0 :
n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ?
1 :
2;
}
},
/**
* Index 8 - Three forms, special cases for 1 and 2, 3, 4
* Slavic family: Czech, Slovak
*/
{
locales: [ 'cs', 'sk' ],
numForms: 3,
get: function(n) {
return n == 1 ?
0 :
n >= 2 && n <= 4 ?
1 :
2;
}
},
/**
* Index 9 - Three forms, special case for one and some numbers ending in
* 2, 3, or 4
* Polish
*/
{
locales: [ 'pl' ],
numForms: 3,
get: function(n) {
return n == 1 ?
0 :
n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ?
1 :
2;
}
},
/**
* Index 10 - Four forms, special case for one and all numbers ending in
* 02, 03, or 04
* Slovenian
*/
{
locales: [ 'sl' ],
numForms: 4,
get: function(n) {
return n % 100 == 1 ?
0 :
n % 100 == 2 ?
1 :
n % 100 == 3 || n % 100 == 4 ?
2 :
3;
}
},
/**
* Index 11 -
* Irish Gaeilge
*/
{
locales: [ 'ga-ie', 'ga-ie', 'ga', 'en-ie' ],
numForms: 5,
get: function(n) {
return n == 1 ?
0 :
n == 2 ?
1 :
n >= 3 && n <= 6 ?
2 :
n >= 7 && n <= 10 ?
3 :
4;
}
},
/**
* Index 12 -
* Arabic
*/
{
locales: [ 'ar' ],
numForms: 6,
get: function(n) {
return n === 0 ?
5 :
n == 1 ?
0 :
n == 2 ?
1 :
n % 100 >= 3 && n % 100 <= 10 ?
2 :
n % 100 >= 11 && n % 100 <= 99 ?
3 :
4;
}
},
/**
* Index 13 -
* Maltese
*/
{
locales: [ 'mt' ],
numForms: 4,
get: function(n) {
return n == 1 ?
0 :
n === 0 || n % 100 > 0 && n % 100 <= 10 ?
1 :
n % 100 > 10 && n % 100 < 20 ?
2 :
3;
}
},
/**
* Index 14 -
* Macedonian
*/
{
locales: [ 'mk', 'mk-mk' ],
numForms: 3,
get: function(n) {
return n % 10 == 1 ?
0 :
n % 10 == 2 ?
1 :
2;
}
},
/**
* Index 15 -
* Icelandic
*/
{
locales: [ 'is' ],
numForms: 2,
get: function(n) {
return n % 10 == 1 && n % 100 != 11 ?
0 :
1;
}
catch (ex) {
console.error('Failed to lookup ', key, ex);
return key;
}
};
/*
// Known locales without a known plural rule
'km', 'ms', 'ne-np', 'ne-np', 'ne', 'nr', 'nr-za', 'rw', 'ss', 'ss-za',
'st', 'st-za', 'tn', 'tn-za', 'ts', 'ts-za', 've', 've-za', 'xh', 'xh-za'
*/
];
/** @see propertyLookup in lib/gcli/util/l10n.js */
exports.propertyLookup = Proxy.create({
get: function(rcvr, name) {
return exports.lookup(name);
}
});
/**
* The cached plural rule
*/
var pluralRule;
/** @see lookupFormat in lib/gcli/util/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;
}
};

View File

@ -16,281 +16,6 @@
'use strict';
/**
* This is a copy of util.errorHandler to avoid dependency loops
*/
var util = {
errorHandler: function(ex) {
if (ex instanceof Error) {
// V8 weirdly includes the exception message in the stack
if (ex.stack.indexOf(ex.message) !== -1) {
console.error(ex.stack);
}
else {
console.error('' + ex);
console.error(ex.stack);
}
}
else {
console.error(ex);
}
}
};
/**
* Internal utility: Wraps given `value` into simplified promise, successfully
* fulfilled to a given `value`. Note the result is not a complete promise
* implementation, as its method `then` does not returns anything.
*/
function fulfilled(value) {
return { then: function then(fulfill) { fulfill(value); } };
}
/**
* Internal utility: Wraps given input into simplified promise, pre-rejected
* with a given `reason`. Note the result is not a complete promise
* implementation, as its method `then` does not returns anything.
*/
function rejected(reason) {
return { then: function then(fulfill, reject) { reject(reason); } };
}
/**
* Internal utility: Returns `true` if given `value` is a promise. Value is
* assumed to be a promise if it implements method `then`.
*/
function isPromise(value) {
return value && typeof(value.then) === 'function';
}
/**
* Creates deferred object containing fresh promise & methods to either resolve
* or reject it. The result is an object with the following properties:
* - `promise` Eventual value representation implementing CommonJS [Promises/A]
* (http://wiki.commonjs.org/wiki/Promises/A) API.
* - `resolve` Single shot function that resolves enclosed `promise` with a
* given `value`.
* - `reject` Single shot function that rejects enclosed `promise` with a given
* `reason`.
*
* An optional `prototype` argument is used as a prototype of the returned
* `promise` allowing one to implement additional API. If prototype is not
* passed then it falls back to `Object.prototype`.
*
* ## Example
*
* function fetchURI(uri, type) {
* var deferred = defer();
* var request = new XMLHttpRequest();
* request.open("GET", uri, true);
* request.responseType = type;
* request.onload = function onload() {
* deferred.resolve(request.response);
* }
* request.onerror = function(event) {
* deferred.reject(event);
* }
* request.send();
*
* return deferred.promise;
* }
*/
function defer(prototype) {
// Define FIFO queue of observer pairs. Once promise is resolved & all queued
// observers are forwarded to `result` and variable is set to `null`.
var observers = [];
// Promise `result`, which will be assigned a resolution value once promise
// is resolved. Note that result will always be assigned promise (or alike)
// object to take care of propagation through promise chains. If result is
// `null` promise is not resolved yet.
var result = null;
prototype = (prototype || prototype === null) ? prototype : Object.prototype;
// Create an object implementing promise API.
var promise = Object.create(prototype, {
then: { value: function then(onFulfill, onError) {
var deferred = defer(prototype);
function resolve(value) {
// If `onFulfill` handler is provided resolve `deferred.promise` with
// result of invoking it with a resolution value. If handler is not
// provided propagate value through.
try {
deferred.resolve(onFulfill ? onFulfill(value) : value);
}
// `onFulfill` may throw exception in which case resulting promise
// is rejected with thrown exception.
catch(error) {
if (exports._reportErrors && typeof(console) === 'object') {
util.errorHandler(error);
}
// Note: Following is equivalent of `deferred.reject(error)`,
// we use this shortcut to reduce a stack.
deferred.resolve(rejected(error));
}
}
function reject(reason) {
try {
if (onError) { deferred.resolve(onError(reason)); }
else { deferred.resolve(rejected(reason)); }
}
catch(error) {
if (exports._reportErrors && typeof(console) === 'object') {
util.errorHandler(error);
}
deferred.resolve(rejected(error));
}
}
// If enclosed promise (`this.promise`) observers queue is still alive
// enqueue a new observer pair into it. Note that this does not
// necessary means that promise is pending, it may already be resolved,
// but we still have to queue observers to guarantee an order of
// propagation.
if (observers) {
observers.push({ resolve: resolve, reject: reject });
}
// Otherwise just forward observer pair right to a `result` promise.
else {
result.then(resolve, reject);
}
return deferred.promise;
}},
done: { value: function() {
this.then(null, util.errorHandler);
}},
});
var deferred = {
promise: promise,
/**
* Resolves associated `promise` to a given `value`, unless it's already
* resolved or rejected. Note that resolved promise is not necessary a
* successfully fulfilled. Promise may be resolved with a promise `value`
* in which case `value` promise's fulfillment / rejection will propagate
* up to a promise resolved with `value`.
*/
resolve: function resolve(value) {
if (!result) {
// Store resolution `value` in a `result` as a promise, so that all
// the subsequent handlers can be simply forwarded to it. Since
// `result` will be a promise all the value / error propagation will
// be uniformly taken care of.
result = isPromise(value) ? value : fulfilled(value);
// Forward already registered observers to a `result` promise in the
// order they were registered. Note that we intentionally dequeue
// observer at a time until queue is exhausted. This makes sure that
// handlers registered as side effect of observer forwarding are
// queued instead of being invoked immediately, guaranteeing FIFO
// order.
while (observers.length) {
var observer = observers.shift();
result.then(observer.resolve, observer.reject);
}
// Once `observers` queue is exhausted we `null`-ify it, so that
// new handlers are forwarded straight to the `result`.
observers = null;
}
},
/**
* Rejects associated `promise` with a given `reason`, unless it's already
* resolved / rejected. This is just a (better performing) convenience
* shortcut for `deferred.resolve(reject(reason))`.
*/
reject: function reject(reason) {
// Note that if promise is resolved that does not necessary means that it
// is successfully fulfilled. Resolution value may be a promise in which
// case its result propagates. In other words if promise `a` is resolved
// with promise `b`, `a` is either fulfilled or rejected depending
// on weather `b` is fulfilled or rejected. Here `deferred.promise` is
// resolved with a promise pre-rejected with a given `reason`, there for
// `deferred.promise` is rejected with a given `reason`. This may feel
// little awkward first, but doing it this way greatly simplifies
// propagation through promise chains.
deferred.resolve(rejected(reason));
}
};
return deferred;
}
exports.defer = defer;
/**
* Returns a promise resolved to a given `value`. Optionally a second
* `prototype` argument may be provided to be used as a prototype for the
* returned promise.
*/
function resolve(value, prototype) {
var deferred = defer(prototype);
deferred.resolve(value);
return deferred.promise;
}
exports.resolve = resolve;
/**
* Returns a promise rejected with a given `reason`. Optionally a second
* `prototype` argument may be provided to be used as a prototype for the
* returned promise.
*/
function reject(reason, prototype) {
var deferred = defer(prototype);
deferred.reject(reason);
return deferred.promise;
}
exports.reject = reject;
var promised = (function() {
// Note: Define shortcuts and utility functions here in order to avoid
// slower property accesses and unnecessary closure creations on each
// call of this popular function.
var call = Function.call;
var concat = Array.prototype.concat;
// Utility function that does following:
// execute([ f, self, args...]) => f.apply(self, args)
function execute(args) { return call.apply(call, args); }
// Utility function that takes promise of `a` array and maybe promise `b`
// as arguments and returns promise for `a.concat(b)`.
function promisedConcat(promises, unknown) {
return promises.then(function(values) {
return resolve(unknown).then(function(value) {
return values.concat([ value ]);
});
});
}
return function promised(f, prototype) {
/**
Returns a wrapped `f`, which when called returns a promise that resolves to
`f(...)` passing all the given arguments to it, which by the way may be
promises. Optionally second `prototype` argument may be provided to be used
a prototype for a returned promise.
## Example
var promise = promised(Array)(1, promise(2), promise(3))
promise.then(console.log) // => [ 1, 2, 3 ]
**/
return function promised() {
// create array of [ f, this, args... ]
return concat.apply([ f, this ], arguments).
// reduce it via `promisedConcat` to get promised array of fulfillments
reduce(promisedConcat, resolve([], prototype)).
// finally map that to promise of `f.apply(this, args...)`
then(execute);
};
};
})();
exports.promised = promised;
var all = promised(Array);
exports.all = all;
var Cu = require('chrome').Cu;
module.exports = exports =
Cu.import('resource://gre/modules/commonjs/sdk/core/promise.js', {}).Promise;

View File

@ -1,163 +0,0 @@
/*
* 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';
var Cc = require('chrome').Cc;
var Ci = require('chrome').Ci;
var Cu = require('chrome').Cu;
/*
* GCLI is built from a number of components (called items) composed as
* required for each environment.
* When adding to or removing from this list, we should keep the basics in sync
* with the other environments.
* See:
* - lib/gcli/index.js: Generic basic set (without commands)
* - lib/gcli/demo.js: Adds demo commands to basic set for use in web demo
* - gcli.js: Add commands to basic set for use in Node command line
* - mozilla/gcli/index.js: From scratch listing for Firefox
* - lib/gcli/connectors/index.js: Client only items when executing remotely
* - lib/gcli/connectors/direct.js: Test items for connecting to in-process GCLI
*/
var items = [
require('./types/delegate').items,
require('./types/selection').items,
require('./types/array').items,
require('./types/boolean').items,
require('./types/command').items,
require('./types/date').items,
require('./types/file').items,
require('./types/javascript').items,
require('./types/node').items,
require('./types/number').items,
require('./types/resource').items,
require('./types/setting').items,
require('./types/string').items,
require('./fields/delegate').items,
require('./fields/selection').items,
require('./ui/focus').items,
require('./ui/intro').items,
require('./converters/converters').items,
require('./converters/basic').items,
// require('./converters/html').items, // Prevent use of innerHTML
require('./converters/terminal').items,
require('./languages/command').items,
require('./languages/javascript').items,
// require('./connectors/direct').items, // No need for loopback testing
// require('./connectors/rdp').items, // Needs fixing
// require('./connectors/websocket').items, // Not from chrome
// require('./connectors/xhr').items, // Not from chrome
// require('./cli').items, // No need for '{' with web console
require('./commands/clear').items,
// require('./commands/connect').items, // We need to fix our RDP connector
require('./commands/context').items,
// require('./commands/exec').items, // No exec in Firefox yet
require('./commands/global').items,
require('./commands/help').items,
// require('./commands/intro').items, // No need for intro command
require('./commands/lang').items,
// require('./commands/mocks').items, // Only for testing
require('./commands/pref').items,
// require('./commands/preflist').items, // Too slow in Firefox
// require('./commands/test').items, // Only for testing
// No demo or node commands
].reduce(function(prev, curr) { return prev.concat(curr); }, []);
var api = require('./api');
api.populateApi(exports);
exports.addItems(items);
var host = require('./util/host');
exports.useTarget = host.script.useTarget;
/**
* This code is internal and subject to change without notice.
* createDisplay() 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('./mozui/ffdisplay').FFDisplay;
return new FFDisplay(opts);
};
var prefSvc = Cc['@mozilla.org/preferences-service;1']
.getService(Ci.nsIPrefService);
var prefBranch = prefSvc.getBranch(null).QueryInterface(Ci.nsIPrefBranch2);
exports.hiddenByChromePref = function() {
return !prefBranch.prefHasUserValue('devtools.chrome.enabled');
};
try {
var Services = Cu.import('resource://gre/modules/Services.jsm', {}).Services;
var stringBundle = Services.strings.createBundle(
'chrome://browser/locale/devtools/gclicommands.properties');
/**
* Lookup a string in the GCLI string bundle
*/
exports.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
*/
exports.lookupFormat = function(name, swaps) {
try {
return stringBundle.formatStringFromName(name, swaps, swaps.length);
}
catch (ex) {
throw new Error('Failure in lookupFormat(\'' + name + '\')');
}
};
}
catch (ex) {
console.error('Using string fallbacks', ex);
exports.lookup = function(name) {
return name;
};
exports.lookupFormat = function(name, swaps) {
return name;
};
}

View File

@ -1,307 +0,0 @@
/*
* 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';
var imports = {};
var Cc = require('chrome').Cc;
var Ci = require('chrome').Ci;
var Cu = require('chrome').Cu;
var XPCOMUtils = Cu.import('resource://gre/modules/XPCOMUtils.jsm', {}).XPCOMUtils;
var Services = Cu.import('resource://gre/modules/Services.jsm', {}).Services;
XPCOMUtils.defineLazyGetter(imports, 'prefBranch', function() {
var prefService = Cc['@mozilla.org/preferences-service;1']
.getService(Ci.nsIPrefService);
return prefService.getBranch(null).QueryInterface(Ci.nsIPrefBranch2);
});
XPCOMUtils.defineLazyGetter(imports, 'supportsString', function() {
return Cc['@mozilla.org/supports-string;1']
.createInstance(Ci.nsISupportsString);
});
var util = require('./util/util');
/**
* All local settings have this prefix when used in Firefox
*/
var DEVTOOLS_PREFIX = 'devtools.gcli.';
/**
* The type library that we use in creating types for settings
*/
var types;
/**
* 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.createType('boolean');
case imports.prefBranch.PREF_INT:
return types.createType('number');
case imports.prefBranch.PREF_STRING:
return types.createType('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,
Ci.nsISupportsString).data;
// In case of a localized string
if (/^chrome:\/\/.+\/locale\/.+\.properties/.test(value)) {
value = imports.prefBranch.getComplexValue(this.name,
Ci.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,
Ci.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);
};
/**
* Collection of preferences for sorted access
*/
var settingsAll = [];
/**
* Collection of preferences for fast indexed access
*/
var settingsMap = new Map();
/**
* Flag so we know if we've read the system preferences
*/
var hasReadSystem = false;
/**
* Clear out all preferences and return to initial state
*/
function reset() {
settingsMap = new Map();
settingsAll = [];
hasReadSystem = false;
}
/**
* Reset everything on startup and shutdown because we're doing lazy loading
*/
exports.startup = function(t) {
reset();
types = t;
if (types == null) {
throw new Error('no types');
}
};
exports.shutdown = function() {
reset();
};
/**
* Load system prefs if they've not been loaded already
* @return true
*/
function readSystem() {
if (hasReadSystem) {
return;
}
imports.prefBranch.getChildList('').forEach(function(name) {
var setting = new Setting(name);
settingsAll.push(setting);
settingsMap.set(name, setting);
});
settingsAll.sort(function(s1, s2) {
return s1.name.localeCompare(s2.name);
});
hasReadSystem = true;
}
/**
* Get an array containing all known Settings filtered to match the given
* filter (string) at any point in the name of the setting
*/
exports.getAll = function(filter) {
readSystem();
if (filter == null) {
return settingsAll;
}
return settingsAll.filter(function(setting) {
return setting.name.indexOf(filter) !== -1;
});
};
/**
* Add a new setting.
*/
exports.addSetting = function(prefSpec) {
var setting = new Setting(prefSpec);
if (settingsMap.has(setting.name)) {
// Once exists already, we're going to need to replace it in the array
for (var i = 0; i < settingsAll.length; i++) {
if (settingsAll[i].name === setting.name) {
settingsAll[i] = setting;
}
}
}
settingsMap.set(setting.name, 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) {
// We might be able to give the answer without needing to read all system
// settings if this is an internal setting
var found = settingsMap.get(name);
if (!found) {
found = settingsMap.get(DEVTOOLS_PREFIX + name);
}
if (found) {
return found;
}
if (hasReadSystem) {
return undefined;
}
else {
readSystem();
found = settingsMap.get(name);
if (!found) {
found = settingsMap.get(DEVTOOLS_PREFIX + name);
}
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() { };

View File

@ -1,11 +0,0 @@
<div>
<table class="gcli-menu-template" aria-live="polite">
<tr class="gcli-menu-option" foreach="item in ${items}"
onclick="${onItemClickInternal}" title="${item.manual}">
<td class="gcli-menu-name">${item.name}</td>
<td class="gcli-menu-desc">${item.description}</td>
</tr>
</table>
<div class="gcli-menu-more" if="${items.hasMore}">${l10n.fieldMenuMore}</div>
</div>

View File

@ -1,21 +0,0 @@
/*
* 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';
var Cu = require('chrome').Cu;
var template = Cu.import('resource://gre/modules/devtools/Templater.jsm', {}).template;
exports.template = template;

View File

@ -1,126 +0,0 @@
/*
* 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';
var Cu = require('chrome').Cu;
var Cc = require('chrome').Cc;
var Ci = require('chrome').Ci;
var OS = Cu.import('resource://gre/modules/osfile.jsm', {}).OS;
var promise = require('./promise');
/**
* A set of functions that don't really belong in 'fs' (because they're not
* really universal in scope) but also kind of do (because they're not specific
* to GCLI
*/
exports.join = OS.Path.join;
exports.sep = OS.Path.sep;
exports.dirname = OS.Path.dirname;
var dirService = Cc['@mozilla.org/file/directory_service;1']
.getService(Ci.nsIProperties);
exports.home = dirService.get('Home', Ci.nsIFile).path;
if ('winGetDrive' in OS.Path) {
exports.sep = '\\';
}
else {
exports.sep = '/';
}
/**
* Split a path into its components.
* @param pathname (string) The part to cut up
* @return An array of path components
*/
exports.split = function(pathname) {
return OS.Path.split(pathname).components;
};
/**
* @param pathname string, path of an existing directory
* @param matches optional regular expression - filter output to include only
* the files that match the regular expression. The regexp is applied to the
* filename only not to the full path
* @return A promise of an array of stat objects for each member of the
* directory pointed to by ``pathname``, each containing 2 extra properties:
* - pathname: The full pathname of the file
* - filename: The final filename part of the pathname
*/
exports.ls = function(pathname, matches) {
var iterator = new OS.File.DirectoryIterator(pathname);
var entries = [];
var iteratePromise = iterator.forEach(function(entry) {
entries.push({
exists: true,
isDir: entry.isDir,
isFile: !entry.isFile,
filename: entry.name,
pathname: entry.path
});
});
return iteratePromise.then(function onSuccess() {
iterator.close();
return entries;
},
function onFailure(reason) {
iterator.close();
throw reason;
}
);
};
/**
* stat() is annoying because it considers stat('/doesnt/exist') to be an
* error, when the point of stat() is to *find* *out*. So this wrapper just
* converts 'ENOENT' i.e. doesn't exist to { exists:false } and adds
* exists:true to stat blocks from existing paths
*/
exports.stat = function(pathname) {
var onResolve = function(stats) {
return {
exists: true,
isDir: stats.isDir,
isFile: !stats.isFile
};
};
var onReject = function(err) {
if (err instanceof OS.File.Error && err.becauseNoSuchFile) {
return {
exists: false,
isDir: false,
isFile: false
};
}
throw err;
};
return OS.File.stat(pathname).then(onResolve, onReject);
};
/**
* We may read the first line of a file to describe it?
* Right now, however, we do nothing.
*/
exports.describe = function(pathname) {
return promise.resolve('');
};

View File

@ -1,209 +0,0 @@
/*
* 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';
var Cu = require('chrome').Cu;
var Cc = require('chrome').Cc;
var Ci = require('chrome').Ci;
var OS = Cu.import('resource://gre/modules/osfile.jsm', {}).OS;
var promise = require('./promise');
var util = require('./util');
function Highlighter(document) {
this._document = document;
this._nodes = util.createEmptyNodeList(this._document);
}
Object.defineProperty(Highlighter.prototype, 'nodelist', {
set: function(nodes) {
Array.prototype.forEach.call(this._nodes, this._unhighlightNode, this);
this._nodes = (nodes == null) ?
util.createEmptyNodeList(this._document) :
nodes;
Array.prototype.forEach.call(this._nodes, this._highlightNode, this);
},
get: function() {
return this._nodes;
},
enumerable: true
});
Highlighter.prototype.destroy = function() {
this.nodelist = null;
};
Highlighter.prototype._highlightNode = function(node) {
// Enable when the highlighter rewrite is done
};
Highlighter.prototype._unhighlightNode = function(node) {
// Enable when the highlighter rewrite is done
};
exports.Highlighter = Highlighter;
/**
* See docs in lib/gcli/util/host.js:exec
*/
exports.exec = function(execSpec) {
throw new Error('Not supported');
};
/**
* Asynchronously load a text resource
* @see lib/gcli/util/host.js
*/
exports.staticRequire = function(requistingModule, name) {
var deferred = promise.defer();
if (name.match(/\.css$/)) {
deferred.resolve('');
}
else {
var filename = OS.Path.dirname(requistingModule.id) + '/' + name;
filename = filename.replace(/\/\.\//g, '/');
filename = 'resource://gre/modules/devtools/' + filename;
var xhr = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
.createInstance(Ci.nsIXMLHttpRequest);
xhr.onload = function onload() {
deferred.resolve(xhr.responseText);
}.bind(this);
xhr.onabort = xhr.onerror = xhr.ontimeout = function(err) {
deferred.reject(err);
}.bind(this);
try {
xhr.open('GET', filename);
xhr.send();
}
catch (ex) {
deferred.reject(ex);
}
}
return deferred.promise;
};
/**
* A group of functions to help scripting. Small enough that it doesn't need
* a separate module (it's basically a wrapper around 'eval' in some contexts)
*/
var client;
var target;
var consoleActor;
var webConsoleClient;
exports.script = { };
exports.script.onOutput = util.createEvent('Script.onOutput');
/**
* Setup the environment to eval JavaScript
*/
exports.script.useTarget = function(tgt) {
target = tgt;
// Local debugging needs to make the target remote.
var targetPromise = target.isRemote ?
promise.resolve(target) :
target.makeRemote();
return targetPromise.then(function() {
var deferred = promise.defer();
client = target._client;
client.addListener('pageError', function(packet) {
if (packet.from === consoleActor) {
// console.log('pageError', packet.pageError);
exports.script.onOutput({
level: 'exception',
message: packet.exception.class
});
}
});
client.addListener('consoleAPICall', function(type, packet) {
if (packet.from === consoleActor) {
var data = packet.message;
var ev = {
level: data.level,
arguments: data.arguments,
};
if (data.filename !== 'debugger eval code') {
ev.source = {
filename: data.filename,
lineNumber: data.lineNumber,
functionName: data.functionName
};
}
exports.script.onOutput(ev);
}
});
consoleActor = target._form.consoleActor;
var onAttach = function(response, wcc) {
webConsoleClient = wcc;
if (response.error != null) {
deferred.reject(response);
}
else {
deferred.resolve(response);
}
// TODO: add _onTabNavigated code?
};
var listeners = [ 'PageError', 'ConsoleAPI' ];
client.attachConsole(consoleActor, listeners, onAttach);
return deferred.promise;
});
};
/**
* Execute some JavaScript
*/
exports.script.eval = function(javascript) {
var deferred = promise.defer();
var onResult = function(response) {
var output = response.result;
if (typeof output === 'object' && output.type === 'undefined') {
output = undefined;
}
deferred.resolve({
input: response.input,
output: output,
exception: response.exception
});
};
webConsoleClient.evaluateJS(javascript, onResult, {});
return deferred.promise;
};

View File

@ -1,89 +0,0 @@
/*
* 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';
var Cu = require('chrome').Cu;
var XPCOMUtils = Cu.import('resource://gre/modules/XPCOMUtils.jsm', {}).XPCOMUtils;
var Services = Cu.import('resource://gre/modules/Services.jsm', {}).Services;
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/util/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/util/l10n.js */
exports.propertyLookup = Proxy.create({
get: function(rcvr, name) {
return exports.lookup(name);
}
});
/** @see lookupFormat in lib/gcli/util/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;
}
};

View File

@ -1,21 +0,0 @@
/*
* 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';
var Cu = require('chrome').Cu;
module.exports = exports =
Cu.import('resource://gre/modules/commonjs/sdk/core/promise.js', {}).Promise;