mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-02-03 12:35:58 +00:00
Merge m-c to m-i
This commit is contained in:
commit
4f870b786b
@ -19,7 +19,7 @@
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="f09ec7d9d0bb7c40998ddb6b5bf397e578add541"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="70978988d048737b1379f5452a679429dadcd35f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="d11f524d00cacf5ba0dfbf25e4aa2158b1c3a036"/>
|
||||
|
@ -17,7 +17,7 @@
|
||||
</project>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="f09ec7d9d0bb7c40998ddb6b5bf397e578add541"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="70978988d048737b1379f5452a679429dadcd35f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="cb16958e41105d7c551d9941f522db97b8312538"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="485846b2a40d8ac7d6c1c5f8af6d15b0c10af19d"/>
|
||||
|
@ -15,7 +15,7 @@
|
||||
<project name="platform_build" path="build" remote="b2g" revision="a9e08b91e9cd1f0930f16cfc49ec72f63575d5fe">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="f09ec7d9d0bb7c40998ddb6b5bf397e578add541"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="70978988d048737b1379f5452a679429dadcd35f"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="cb16958e41105d7c551d9941f522db97b8312538"/>
|
||||
|
@ -19,7 +19,7 @@
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="f09ec7d9d0bb7c40998ddb6b5bf397e578add541"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="70978988d048737b1379f5452a679429dadcd35f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="d11f524d00cacf5ba0dfbf25e4aa2158b1c3a036"/>
|
||||
|
@ -4,6 +4,6 @@
|
||||
"branch": "",
|
||||
"revision": ""
|
||||
},
|
||||
"revision": "a7c17d6cce9c60631e386b75885199a59b555a43",
|
||||
"revision": "1b537b86025c437cd428abbc2996f376f3b78799",
|
||||
"repo_path": "/integration/gaia-central"
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="f09ec7d9d0bb7c40998ddb6b5bf397e578add541"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="70978988d048737b1379f5452a679429dadcd35f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1f6a1fe07f81c5bc5e1d079c9b60f7f78ca2bf4f"/>
|
||||
|
@ -15,7 +15,7 @@
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="f09ec7d9d0bb7c40998ddb6b5bf397e578add541"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="70978988d048737b1379f5452a679429dadcd35f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1f6a1fe07f81c5bc5e1d079c9b60f7f78ca2bf4f"/>
|
||||
|
@ -19,7 +19,7 @@
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="f09ec7d9d0bb7c40998ddb6b5bf397e578add541"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="70978988d048737b1379f5452a679429dadcd35f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1f6a1fe07f81c5bc5e1d079c9b60f7f78ca2bf4f"/>
|
||||
|
@ -17,7 +17,7 @@
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="f09ec7d9d0bb7c40998ddb6b5bf397e578add541"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="70978988d048737b1379f5452a679429dadcd35f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1f6a1fe07f81c5bc5e1d079c9b60f7f78ca2bf4f"/>
|
||||
|
@ -17,7 +17,7 @@
|
||||
</project>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="f09ec7d9d0bb7c40998ddb6b5bf397e578add541"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="70978988d048737b1379f5452a679429dadcd35f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="cb16958e41105d7c551d9941f522db97b8312538"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="485846b2a40d8ac7d6c1c5f8af6d15b0c10af19d"/>
|
||||
|
@ -17,7 +17,7 @@
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="f09ec7d9d0bb7c40998ddb6b5bf397e578add541"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="70978988d048737b1379f5452a679429dadcd35f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1f6a1fe07f81c5bc5e1d079c9b60f7f78ca2bf4f"/>
|
||||
|
@ -1209,7 +1209,7 @@ pref("devtools.shadereditor.enabled", false);
|
||||
pref("devtools.chrome.enabled", false);
|
||||
|
||||
// Default theme ("dark" or "light")
|
||||
pref("devtools.theme", "dark");
|
||||
pref("devtools.theme", "light");
|
||||
|
||||
// Display the introductory text
|
||||
pref("devtools.gcli.hideIntro", false);
|
||||
|
@ -1,19 +1,7 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#remote {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@ -24,27 +12,3 @@ html, body {
|
||||
#manage, #intro, #stage {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#stage {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.graphic-sync-intro {
|
||||
background-image: url(images/graphic_sync_intro.png);
|
||||
}
|
||||
|
||||
.description, .button-row {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
/* Retina */
|
||||
@media
|
||||
only screen and ( min--moz-device-pixel-ratio: 2),
|
||||
only screen and ( -moz-min-device-pixel-ratio: 2),
|
||||
only screen and ( min-device-pixel-ratio: 2),
|
||||
only screen and ( min-resolution: 192dpi),
|
||||
only screen and ( min-resolution: 2dppx) {
|
||||
.graphic-sync-intro {
|
||||
background-image: url(images/graphic_sync_intro@2x.png);
|
||||
}
|
||||
}
|
||||
|
@ -81,6 +81,15 @@ function promptForRelink(acctName) {
|
||||
return pressed == 0; // 0 is the "continue" button
|
||||
}
|
||||
|
||||
// If the last fxa account used for sync isn't this account, we display
|
||||
// a modal dialog checking they really really want to do this...
|
||||
// (This is sync-specific, so ideally would be in sync's identity module,
|
||||
// but it's a little more seamless to do here, and sync is currently the
|
||||
// only fxa consumer, so...
|
||||
function shouldAllowRelink(acctName) {
|
||||
return !needRelinkWarning(acctName) || promptForRelink(acctName);
|
||||
}
|
||||
|
||||
let wrapper = {
|
||||
iframe: null,
|
||||
|
||||
@ -132,20 +141,20 @@ let wrapper = {
|
||||
delete accountData.customizeSync;
|
||||
}
|
||||
|
||||
// If the last fxa account used for sync isn't this account, we display
|
||||
// a modal dialog checking they really really want to do this...
|
||||
// (This is sync-specific, so ideally would be in sync's identity module,
|
||||
// but it's a little more seamless to do here, and sync is currently the
|
||||
// only fxa consumer, so...
|
||||
// We need to confirm a relink - see shouldAllowRelink for more
|
||||
let newAccountEmail = accountData.email;
|
||||
if (needRelinkWarning(newAccountEmail) && !promptForRelink(newAccountEmail)) {
|
||||
// The hosted code may have already checked for the relink situation
|
||||
// by sending the can_link_account command. If it did, then
|
||||
// it will indicate we don't need to ask twice.
|
||||
if (!accountData.verifiedCanLinkAccount && !shouldAllowRelink(newAccountEmail)) {
|
||||
// we need to tell the page we successfully received the message, but
|
||||
// then bail without telling fxAccounts
|
||||
this.injectData("message", { status: "login" });
|
||||
// and reload the page or else it remains in a "signed in" state.
|
||||
window.location.reload();
|
||||
// and re-init the page by navigating to about:accounts
|
||||
window.location = "about:accounts";
|
||||
return;
|
||||
}
|
||||
delete accountData.verifiedCanLinkAccount;
|
||||
|
||||
// Remember who it was so we can log out next time.
|
||||
setPreviousAccountNameHash(newAccountEmail);
|
||||
@ -175,6 +184,12 @@ let wrapper = {
|
||||
);
|
||||
},
|
||||
|
||||
onCanLinkAccount: function(accountData) {
|
||||
// We need to confirm a relink - see shouldAllowRelink for more
|
||||
let ok = shouldAllowRelink(accountData.email);
|
||||
this.injectData("message", { status: "can_link_account", data: { ok: ok } });
|
||||
},
|
||||
|
||||
/**
|
||||
* onSessionStatus sends the currently signed in user's credentials
|
||||
* to the jelly.
|
||||
@ -208,6 +223,9 @@ let wrapper = {
|
||||
case "login":
|
||||
this.onLogin(data);
|
||||
break;
|
||||
case "can_link_account":
|
||||
this.onCanLinkAccount(data);
|
||||
break;
|
||||
case "session_status":
|
||||
this.onSessionStatus(data);
|
||||
break;
|
||||
|
@ -39,8 +39,7 @@
|
||||
|
||||
<div id="manage">
|
||||
<header>
|
||||
<h1><span>&syncBrand.fullName.label;</span></h1>
|
||||
<h2><span>&aboutAccounts.welcome;</span></h2>
|
||||
<h1>&aboutAccounts.welcome;</h1>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
@ -54,8 +53,7 @@
|
||||
|
||||
<div id="intro">
|
||||
<header>
|
||||
<h1><span>&syncBrand.fullName.label;</span></h1>
|
||||
<h2><span>&aboutAccounts.welcome;</span></h2>
|
||||
<h1>&aboutAccounts.welcome;</h1>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
@ -68,7 +66,7 @@
|
||||
</div>
|
||||
|
||||
<div class="links">
|
||||
<a id="oldsync" href="#" onclick="handleOldSync();">&aboutAccountsConfig.useOldSync.label;</a>
|
||||
<a id="oldsync" class="no-underline" href="#" onclick="handleOldSync();">&aboutAccountsConfig.useOldSync.label;</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 9.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 19 KiB |
@ -1,185 +1,82 @@
|
||||
/* Border box all the things by default */
|
||||
*, *:before, *:after {
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: #f1f1f1;
|
||||
background-image: -webkit-linear-gradient(-90deg, #fefefe 0%, #f1f1f1 100%);
|
||||
background-image: -moz-linear-gradient(-90deg, #fefefe 0%, #f1f1f1 100%);
|
||||
background-image: -ms-linear-gradient(-90deg, #fefefe 0%, #f1f1f1 100%);
|
||||
background-image: -o-linear-gradient(-90deg, #fefefe 0%, #f1f1f1 100%);
|
||||
background-image: linear-gradient(-180deg, #fefefe 0%, #f1f1f1 100%);
|
||||
background-repeat: no-repeat;
|
||||
background-color: #F2F2F2;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #424F59;
|
||||
color: #424f59;
|
||||
font-family: "Clear Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0096DD;
|
||||
color: #0095dd;
|
||||
cursor: pointer; /* Use the correct cursor for anchors without an href */
|
||||
}
|
||||
|
||||
noscript {
|
||||
color: #f00;
|
||||
display: block;
|
||||
margin: 160px 0 0 0;
|
||||
a:active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a:focus {
|
||||
outline: 1px dotted #0095dd;
|
||||
}
|
||||
|
||||
|
||||
a.no-underline {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#stage {
|
||||
background: #fff url(images/fox.png) no-repeat left 50% bottom 30px;
|
||||
background-size: 30px 31px;
|
||||
-webkit-border-radius: 10px;
|
||||
border-radius: 10px;
|
||||
-webkit-box-shadow: 0px 1px 3px 0px rgba(0,0,0,0.25);;
|
||||
box-shadow: 0px 1px 3px 0px rgba(0,0,0,0.25);
|
||||
margin: 60px auto 0;
|
||||
min-height: 550px;
|
||||
opacity: 0;
|
||||
/* padding-bottom = 30px (below icon) + 31px (firefox icon) + 40px (above icon) */
|
||||
padding: 50px 25px 101px 25px;
|
||||
text-align: center;
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
header h1, header h2 {
|
||||
font-family: "Fira Sans", Helvetica, Arial, sans-serif;
|
||||
line-height: 1em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 24px;
|
||||
font-weight: normal;
|
||||
margin-bottom: 21px;
|
||||
}
|
||||
|
||||
header h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 200;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
section p {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #D63920;
|
||||
color: #fff;
|
||||
margin-bottom: 5px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.error:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.graphic {
|
||||
background-repeat: none;
|
||||
background-size: 150px 130px;
|
||||
height: 130px;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
text-indent: 100%;
|
||||
white-space: nowrap;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.graphic-checkbox {
|
||||
background-image: url(../images/graphic_checkbox.png);
|
||||
}
|
||||
|
||||
.graphic-mail {
|
||||
background-image: url(../images/graphic_mail.png);
|
||||
}
|
||||
|
||||
.input-row {
|
||||
margin-bottom: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-row input, .input-row select {
|
||||
border: 1px solid #C0C9D0;
|
||||
-webkit-border-radius: 5px;
|
||||
background:#fff;
|
||||
border-radius: 5px;
|
||||
color: #454f59;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.25);
|
||||
margin: 0 auto;
|
||||
min-height: 300px;
|
||||
padding: 60px 40px 40px 40px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
top: 80px;
|
||||
width: 420px;
|
||||
}
|
||||
|
||||
header h1
|
||||
{
|
||||
font-family: "Fira Sans", Helvetica, Arial, sans-serif;
|
||||
font-size: 24px;
|
||||
font-weight: 200;
|
||||
line-height: 1em;
|
||||
margin: 0 0 32px 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 18px;
|
||||
height: 45px;
|
||||
padding: 0 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-row input::-webkit-input-placeholder {
|
||||
color: #C0C9D0;
|
||||
}
|
||||
|
||||
.input-row input:-moz-placeholder { /* Firefox 18- */
|
||||
color: #C0C9D0;
|
||||
}
|
||||
|
||||
.input-row input::-moz-placeholder { /* Firefox 19+ */
|
||||
color: #C0C9D0;
|
||||
}
|
||||
|
||||
.input-row input:-ms-input-placeholder {
|
||||
color: #C0C9D0;
|
||||
}
|
||||
|
||||
.input-row select {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
-moz-user-select: none;
|
||||
appearance: none;
|
||||
background: transparent url(../images/ddarrow_inactive.png) no-repeat right 14px top 50%;
|
||||
background-size: 10px 17px;
|
||||
color: #C0C9D0;
|
||||
height: auto;
|
||||
outline: none;
|
||||
padding: 8px 20px;
|
||||
|
||||
/* Some hackery for Firefox since moz-appearance: none doesn't remove the arrow */
|
||||
text-indent: 0.01px;
|
||||
text-overflow: "";
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-row input:focus, .input-row select:focus {
|
||||
border-color: #6f7a85;
|
||||
}
|
||||
|
||||
.input-row select:focus {
|
||||
background-image: url(../images/ddarrow_active.png);
|
||||
color: #454f59;
|
||||
}
|
||||
|
||||
.input-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.input-row .input-help {
|
||||
margin-top: 10px;
|
||||
color: #C0C9D0;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
margin-bottom: 20px;
|
||||
margin-top: 45px;
|
||||
margin-bottom:20px;
|
||||
}
|
||||
|
||||
.button-row button, .button-row a.button {
|
||||
background: #E66000;
|
||||
.button-row button,
|
||||
.button-row a.button {
|
||||
background: #0095dd;
|
||||
border: none;
|
||||
-webkit-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
color: #fff;
|
||||
color: #FFFFFF;
|
||||
cursor: pointer;
|
||||
font-family: "Clear Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 24px;
|
||||
padding: 15px 0;
|
||||
transition-duration: 150ms;
|
||||
transition-property: background-color;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@ -188,199 +85,68 @@ section p {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.button-row a.button:active,
|
||||
.button-row a.button:hover,
|
||||
.button-row a.button:focus,
|
||||
.button-row button:active,
|
||||
.button-row button:hover,
|
||||
.button-row button:focus {
|
||||
background: #FF9500;
|
||||
background: #08c;
|
||||
}
|
||||
|
||||
.button-row button:disabled {
|
||||
background: #C0C9D0;
|
||||
}
|
||||
|
||||
/* Custom rows */
|
||||
.input-row-month-day select {
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.links {
|
||||
margin-bottom: 20px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.links a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.links a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.links .left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.links .right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.privacy-links, .privacy-links a {
|
||||
color: #C0C9D0;
|
||||
margin-top: 35px;
|
||||
}
|
||||
|
||||
.privacy-links + .button-row {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.verification-email-message {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.password-row {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.password-row > .password {
|
||||
padding-right: 75px;
|
||||
}
|
||||
|
||||
.password-row > .password + label {
|
||||
border-left: 1px solid #C0C9D0;
|
||||
color: #C0C9D0;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
/* it is very easy to accidentally select the text when clicking */
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
.password-row > .password:focus + label {
|
||||
border-color: #6F7A85;
|
||||
}
|
||||
|
||||
.password-row > .password + label > input[type=checkbox] {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.links + .privacy-links {
|
||||
margin: 20px 0 15px 0;
|
||||
}
|
||||
|
||||
.cannot-create-account-content {
|
||||
margin-top: 105px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
-webkit-animation: 0.9s spin infinite linear;
|
||||
-moz-animation: 0.9s spin infinite linear;
|
||||
animation: 0.9s spin infinite linear;
|
||||
background-image: url(../images/spinnerlight.png);
|
||||
background-size: 30px 30px;
|
||||
display: block;
|
||||
margin: 130px auto 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
.graphic-sync-intro {
|
||||
background-image: url(images/graphic_sync_intro.png);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 150px 195px;
|
||||
height: 195px;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
text-indent: 100%;
|
||||
white-space: nowrap;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
@-webkit-keyframes spin {
|
||||
from {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: rotate(365deg);
|
||||
}
|
||||
.description,
|
||||
.button-row {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
@-moz-keyframes spin {
|
||||
from {
|
||||
-moz-transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
-moz-transform: rotate(365deg);
|
||||
}
|
||||
.links {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(365deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO: These need further consideration */
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Responsiveness */
|
||||
|
||||
/* TODO: Confirm breakpoint sizes */
|
||||
@media only screen and (max-width: 500px) {
|
||||
html {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#stage {
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
margin: 0 auto;
|
||||
margin: 30px auto 0 auto;
|
||||
min-height: none;
|
||||
min-width: 320px;
|
||||
padding: 0 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.button-row button,
|
||||
.button-row a.button {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Retina */
|
||||
|
||||
@media
|
||||
only screen and (-webkit-min-device-pixel-ratio: 2),
|
||||
only screen and ( min--moz-device-pixel-ratio: 2),
|
||||
only screen and ( -moz-min-device-pixel-ratio: 2),
|
||||
only screen and ( -o-min-device-pixel-ratio: 2/1),
|
||||
only screen and ( min-device-pixel-ratio: 2),
|
||||
only screen and ( min-resolution: 192dpi),
|
||||
only screen and ( min-resolution: 2dppx) {
|
||||
#stage {
|
||||
background-image: url(../images/fox@2x.png);
|
||||
}
|
||||
|
||||
.graphic-checkbox {
|
||||
background-image: url(../images/graphic_checkbox@2x.png);
|
||||
}
|
||||
|
||||
.graphic-mail {
|
||||
background-image: url(../images/graphic_mail@2x.png);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
background-image: url(../images/spinnerlight@2x.png);
|
||||
}
|
||||
|
||||
.input-row select {
|
||||
background-image: url(../images/ddarrow_inactive@2x.png)
|
||||
}
|
||||
|
||||
.input-row select:focus {
|
||||
background-image: url(../images/ddarrow_active@2x.png);
|
||||
only screen and (min-device-pixel-ratio: 2),
|
||||
only screen and ( min-resolution: 192dpi),
|
||||
only screen and ( min-resolution: 2dppx) {
|
||||
.graphic-sync-intro {
|
||||
background-image: url(images/graphic_sync_intro@2x.png);
|
||||
}
|
||||
}
|
||||
|
@ -405,16 +405,30 @@ let LightweightThemeListener = {
|
||||
updateStyleSheet: function(headerImage) {
|
||||
if (!this.styleSheet)
|
||||
return;
|
||||
for (let i = 0; i < this.styleSheet.cssRules.length; i++) {
|
||||
let rule = this.styleSheet.cssRules[i];
|
||||
if (!rule.style.backgroundImage)
|
||||
continue;
|
||||
this.substituteRules(this.styleSheet.cssRules, headerImage);
|
||||
},
|
||||
|
||||
if (!this._modifiedStyles[i])
|
||||
this._modifiedStyles[i] = { backgroundImage: rule.style.backgroundImage };
|
||||
substituteRules: function(ruleList, headerImage, existingStyleRulesModified = 0) {
|
||||
let styleRulesModified = 0;
|
||||
for (let i = 0; i < ruleList.length; i++) {
|
||||
let rule = ruleList[i];
|
||||
if (rule instanceof Ci.nsIDOMCSSGroupingRule) {
|
||||
// Add the number of modified sub-rules to the modified count
|
||||
styleRulesModified += this.substituteRules(rule.cssRules, headerImage, existingStyleRulesModified + styleRulesModified);
|
||||
} else if (rule instanceof Ci.nsIDOMCSSStyleRule) {
|
||||
if (!rule.style.backgroundImage)
|
||||
continue;
|
||||
let modifiedIndex = existingStyleRulesModified + styleRulesModified;
|
||||
if (!this._modifiedStyles[modifiedIndex])
|
||||
this._modifiedStyles[modifiedIndex] = { backgroundImage: rule.style.backgroundImage };
|
||||
|
||||
rule.style.backgroundImage = this._modifiedStyles[i].backgroundImage + ", " + headerImage;
|
||||
rule.style.backgroundImage = this._modifiedStyles[modifiedIndex].backgroundImage + ", " + headerImage;
|
||||
styleRulesModified++;
|
||||
} else {
|
||||
Cu.reportError("Unsupported rule encountered");
|
||||
}
|
||||
}
|
||||
return styleRulesModified;
|
||||
},
|
||||
|
||||
// nsIObserver
|
||||
|
@ -1219,8 +1219,16 @@ let CustomizableUIInternal = {
|
||||
}
|
||||
} else if (aWidget.type == "view") {
|
||||
let ownerWindow = aNode.ownerDocument.defaultView;
|
||||
ownerWindow.PanelUI.showSubView(aWidget.viewId, aNode,
|
||||
this.getPlacementOfWidget(aNode.id).area);
|
||||
let area = this.getPlacementOfWidget(aNode.id).area;
|
||||
let anchor = aNode;
|
||||
if (area != CustomizableUI.AREA_PANEL) {
|
||||
let wrapper = this.wrapWidget(aWidget.id).forWindow(ownerWindow);
|
||||
if (wrapper && wrapper.anchor) {
|
||||
this.hidePanelForNode(aNode);
|
||||
anchor = wrapper.anchor;
|
||||
}
|
||||
}
|
||||
ownerWindow.PanelUI.showSubView(aWidget.viewId, anchor, area);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -297,6 +297,19 @@ const DownloadsIndicatorView = {
|
||||
*/
|
||||
_notificationTimeout: null,
|
||||
|
||||
/**
|
||||
* Check if the panel containing aNode is open.
|
||||
* @param aNode
|
||||
* the node whose panel we're interested in.
|
||||
*/
|
||||
_isAncestorPanelOpen: function DIV_isAncestorPanelOpen(aNode)
|
||||
{
|
||||
while (aNode && aNode.localName != "panel") {
|
||||
aNode = aNode.parentNode;
|
||||
}
|
||||
return aNode && aNode.state == "open";
|
||||
},
|
||||
|
||||
/**
|
||||
* If the status indicator is visible in its assigned position, shows for a
|
||||
* brief time a visual notification of a relevant event, like a new download.
|
||||
@ -323,8 +336,8 @@ const DownloadsIndicatorView = {
|
||||
let widgetGroup = CustomizableUI.getWidget("downloads-button");
|
||||
let widgetInWindow = widgetGroup.forWindow(window);
|
||||
if (widgetInWindow.overflowed || widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
|
||||
if (anchor && isElementVisible(anchor.parentNode)) {
|
||||
// If the panel is open, don't do anything:
|
||||
if (anchor && this._isAncestorPanelOpen(anchor)) {
|
||||
// If the containing panel is open, don't do anything:
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -259,26 +259,29 @@
|
||||
|
||||
<groupbox id="syncOptions">
|
||||
<caption label="&syncBrand.shortName.label;"/>
|
||||
<vbox id="fxaSyncEngines">
|
||||
<checkbox label="&engine.tabs.label;"
|
||||
accesskey="&engine.tabs.accesskey;"
|
||||
preference="engine.tabs"/>
|
||||
<checkbox label="&engine.bookmarks.label;"
|
||||
accesskey="&engine.bookmarks.accesskey;"
|
||||
preference="engine.bookmarks"/>
|
||||
<checkbox label="&engine.passwords.label;"
|
||||
accesskey="&engine.passwords.accesskey;"
|
||||
preference="engine.passwords"/>
|
||||
<checkbox label="&engine.history.label;"
|
||||
accesskey="&engine.history.accesskey;"
|
||||
preference="engine.history"/>
|
||||
<checkbox label="&engine.addons.label;"
|
||||
accesskey="&engine.addons.accesskey;"
|
||||
preference="engine.addons"/>
|
||||
<checkbox label="&engine.prefs.label;"
|
||||
accesskey="&engine.prefs.accesskey;"
|
||||
preference="engine.prefs"/>
|
||||
</vbox>
|
||||
<hbox id="fxaSyncEngines">
|
||||
<vbox>
|
||||
<checkbox label="&engine.tabs.label;"
|
||||
accesskey="&engine.tabs.accesskey;"
|
||||
preference="engine.tabs"/>
|
||||
<checkbox label="&engine.bookmarks.label;"
|
||||
accesskey="&engine.bookmarks.accesskey;"
|
||||
preference="engine.bookmarks"/>
|
||||
<checkbox label="&engine.passwords.label;"
|
||||
accesskey="&engine.passwords.accesskey;"
|
||||
preference="engine.passwords"/>
|
||||
<checkbox label="&engine.history.label;"
|
||||
accesskey="&engine.history.accesskey;"
|
||||
preference="engine.history"/>
|
||||
<checkbox label="&engine.addons.label;"
|
||||
accesskey="&engine.addons.accesskey;"
|
||||
preference="engine.addons"/>
|
||||
<checkbox label="&engine.prefs.label;"
|
||||
accesskey="&engine.prefs.accesskey;"
|
||||
preference="engine.prefs"/>
|
||||
</vbox>
|
||||
<spacer/>
|
||||
</hbox>
|
||||
</groupbox>
|
||||
<hbox align="center">
|
||||
<label value="&syncDeviceName.label;"
|
||||
|
@ -596,9 +596,9 @@ function prettifyCSS(text)
|
||||
|
||||
if (shouldIndent) {
|
||||
let la = text[i+1]; // one-character lookahead
|
||||
if (!/\s/.test(la)) {
|
||||
// following character should be a new line (or whitespace) but it isn't
|
||||
// force indentation then
|
||||
if (!/\n/.test(la) || /^\s+$/.test(text.substring(i+1, text.length))) {
|
||||
// following character should be a new line, but isn't,
|
||||
// or it's whitespace at the end of the file
|
||||
parts.push(indent + text.substring(partStart, i + 1));
|
||||
if (c == "}") {
|
||||
parts.push(""); // for extra line separator
|
||||
|
@ -14,6 +14,7 @@ support-files =
|
||||
media.html
|
||||
minified.html
|
||||
nostyle.html
|
||||
pretty.css
|
||||
resources_inpage.jsi
|
||||
resources_inpage1.css
|
||||
resources_inpage2.css
|
||||
@ -45,8 +46,6 @@ skip-if = os == "linux" || "mac" # bug 949355
|
||||
[browser_styleeditor_new.js]
|
||||
[browser_styleeditor_nostyle.js]
|
||||
[browser_styleeditor_pretty.js]
|
||||
# Disabled because of intermittent failures - See Bug 942473
|
||||
skip-if = true
|
||||
[browser_styleeditor_private_perwindowpb.js]
|
||||
[browser_styleeditor_reload.js]
|
||||
[browser_styleeditor_sv_keynav.js]
|
||||
|
@ -2,9 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>minified testcase</title>
|
||||
<style type="text/css"><!--
|
||||
body{background:white;}div{font-size:4em;color:red}
|
||||
--></style>
|
||||
<link rel="stylesheet" href="pretty.css"/>
|
||||
<style type="text/css">body { background: red; }
|
||||
div {
|
||||
font-size: 5em;
|
||||
|
5
browser/devtools/styleeditor/test/pretty.css
Normal file
5
browser/devtools/styleeditor/test/pretty.css
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
body{background:white;}div{font-size:4em;color:red}
|
||||
|
||||
|
@ -26,3 +26,11 @@
|
||||
lwtHeader;*/
|
||||
background-position: 0 0, 0 0, right top;
|
||||
}
|
||||
|
||||
@media (min-resolution: 2dppx) {
|
||||
#tabbrowser-tabs:not([movingtab]) > .tabbrowser-tab > .tab-stack > .tab-background > .tab-background-middle[selected=true]:-moz-lwtheme {
|
||||
background-image: url(chrome://browser/skin/tabbrowser/tab-active-middle@2x.png),
|
||||
@fgTabTextureLWT@;/*,
|
||||
lwtHeader;*/
|
||||
}
|
||||
}
|
||||
|
@ -117,18 +117,59 @@
|
||||
/**
|
||||
* In the classic themes, the titlebar has a horizontal gradient, which is
|
||||
* problematic for reading the text of background tabs when they're in the
|
||||
* titlebar. We side-step this issue by layering our own gradient underneath
|
||||
* the tabs.
|
||||
* titlebar. We side-step this issue by layering our own background underneath
|
||||
* the tabs. Unfortunately, this requires a bunch of positioning in order to get
|
||||
* text and icons to not appear fuzzy.
|
||||
*/
|
||||
@media (-moz-windows-classic) {
|
||||
#main-window[tabsintitlebar]:not([inFullscreen]) #TabsToolbar:not(:-moz-lwtheme) {
|
||||
background: linear-gradient(to top, @toolbarShadowColor@ 2px, transparent 2px),
|
||||
linear-gradient(rgba(50%,50%,50%,0), ActiveCaption 85%);
|
||||
#main-window[tabsintitlebar]:not([sizemode=fullscreen]) .tabbrowser-tab:not([selected=true]),
|
||||
#main-window[tabsintitlebar]:not([sizemode=fullscreen]) .tabs-newtab-button {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#main-window[tabsintitlebar]:not([inFullscreen]) #TabsToolbar:not(:-moz-lwtheme):-moz-window-inactive {
|
||||
background: linear-gradient(to top, @toolbarShadowColor@ 2px, transparent 2px),
|
||||
linear-gradient(rgba(50%,50%,50%,0), InactiveCaption 85%);
|
||||
#main-window[tabsintitlebar] #TabsToolbar:not(:-moz-lwtheme) {
|
||||
background-image: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#main-window[tabsintitlebar]:not([sizemode=fullscreen]) #TabsToolbar:not(:-moz-lwtheme)::after {
|
||||
/* Because we use placeholders for window controls etc. in the tabstrip,
|
||||
* and position those with ordinal attributes, and because our layout code
|
||||
* expects :before/:after nodes to come first/last in the frame list,
|
||||
* we have to reorder this element to come last, hence the
|
||||
* ordinal group value (see bug 853415). */
|
||||
-moz-box-ordinal-group: 1001;
|
||||
box-shadow: 0 0 50px 8px ActiveCaption;
|
||||
content: "";
|
||||
display: -moz-box;
|
||||
height: 0;
|
||||
margin: 0 50px;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
top: 100%;
|
||||
width: -moz-available;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
#main-window[tabsintitlebar]:not([sizemode=fullscreen]) #TabsToolbar:not(:-moz-lwtheme):-moz-window-inactive::after {
|
||||
box-shadow: 0 0 50px 8px InactiveCaption;
|
||||
}
|
||||
|
||||
/* Need to constrain the box shadow fade to avoid overlapping layers, see bug 886281. */
|
||||
#main-window[tabsintitlebar]:not([sizemode=fullscreen]) #navigator-toolbox:not(:-moz-lwtheme) {
|
||||
overflow: -moz-hidden-unscrollable;
|
||||
}
|
||||
|
||||
#main-window[tabsintitlebar][sizemode=normal] .tabbrowser-arrowscrollbox > .arrowscrollbox-scrollbox > .scrollbox-innerbox:not(:-moz-lwtheme) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#main-window[tabsintitlebar]:not([sizemode=fullscreen]) #TabsToolbar .toolbarbutton-1,
|
||||
#main-window[tabsintitlebar]:not([sizemode=fullscreen]) .tabbrowser-arrowscrollbox > .scrollbutton-up,
|
||||
#main-window[tabsintitlebar]:not([sizemode=fullscreen]) .tabbrowser-arrowscrollbox > .scrollbutton-down {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#main-window[tabsintitlebar][sizemode="normal"] #titlebar-content:-moz-lwtheme {
|
||||
@ -922,6 +963,8 @@ toolbarbutton[sdk-button="true"][cui-areatype="toolbar"] > .toolbarbutton-icon {
|
||||
#main-window[tabsintitlebar]:not([inFullscreen]) :-moz-any(#TabsToolbar, #toolbar-menubar) > toolbarpaletteitem > :-moz-any(@primaryToolbarButtons@):-moz-system-metric(windows-classic):not(:-moz-lwtheme),
|
||||
#main-window[tabsintitlebar]:not([inFullscreen]) :-moz-any(#TabsToolbar, #toolbar-menubar) > :-moz-any(@primaryToolbarButtons@):-moz-system-metric(windows-classic):not(:-moz-lwtheme),
|
||||
#home-button.bookmark-item:-moz-lwtheme-brighttext {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
list-style-image: url("chrome://browser/skin/Toolbar-inverted.png");
|
||||
}
|
||||
|
||||
|
@ -195,7 +195,7 @@ MobileMessageManager::Send(JS::Handle<JS::Value> aNumber,
|
||||
rv = smsService->GetSmsDefaultServiceId(&serviceId);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
if (aArgc == 3) {
|
||||
if (aArgc == 1) {
|
||||
JS::Rooted<JS::Value> param(aCx, aSendParams);
|
||||
RootedDictionary<SmsSendParameters> sendParams(aCx);
|
||||
if (!sendParams.Init(aCx, param)) {
|
||||
@ -265,7 +265,7 @@ MobileMessageManager::SendMMS(JS::Handle<JS::Value> aParams,
|
||||
nsresult rv = mmsService->GetMmsDefaultServiceId(&serviceId);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
if (aArgc == 2) {
|
||||
if (aArgc == 1) {
|
||||
JS::Rooted<JS::Value> param(aCx, aSendParams);
|
||||
RootedDictionary<MmsSendParameters> sendParams(aCx);
|
||||
if (!sendParams.Init(aCx, param)) {
|
||||
|
@ -182,6 +182,8 @@ not_android_res_files := \
|
||||
*.mkdir.done* \
|
||||
*.DS_Store* \
|
||||
*\#* \
|
||||
*.rej \
|
||||
*.orig \
|
||||
$(NULL)
|
||||
|
||||
# This uses the fact that Android resource directories list all
|
||||
@ -253,9 +255,10 @@ gecko-nodeps/R.txt: .aapt.nodeps ;
|
||||
# This ignores the default set of resources ignored by aapt, plus
|
||||
# files starting with '#'. (Emacs produces temp files named #temp#.)
|
||||
# This doesn't actually set the environment variable; it's used as a
|
||||
# parameter in the aapt invocation below.
|
||||
# parameter in the aapt invocation below. Consider updating
|
||||
# not_android_res_files as well.
|
||||
|
||||
ANDROID_AAPT_IGNORE := !.svn:!.git:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*.scc:*~:\#*
|
||||
ANDROID_AAPT_IGNORE := !.svn:!.git:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*.scc:*~:\#*:*.rej:*.orig
|
||||
|
||||
# 1: target file.
|
||||
# 2: dependencies.
|
||||
|
@ -18,6 +18,7 @@ import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.parser.ParseException;
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
import org.mozilla.gecko.sync.crypto.CryptoException;
|
||||
@ -374,6 +375,7 @@ public class GlobalSession implements PrefsSource, HttpResponseObserver {
|
||||
}
|
||||
|
||||
public void updateMetaGlobalInPlace() {
|
||||
config.metaGlobal.declined = this.declinedEngineNames();
|
||||
ExtendedJSONObject engines = config.metaGlobal.getEngines();
|
||||
for (Entry<String, EngineSettings> pair : enginesToUpdate.entrySet()) {
|
||||
if (pair.getValue() == null) {
|
||||
@ -651,6 +653,38 @@ public class GlobalSession implements PrefsSource, HttpResponseObserver {
|
||||
Utils.toCommaSeparatedString(config.enabledEngineNames) + "' from meta/global.");
|
||||
}
|
||||
}
|
||||
|
||||
// Persist declined.
|
||||
// Our declined engines at any point are:
|
||||
// Whatever they were remotely, plus whatever they were locally, less any
|
||||
// engines that were just enabled locally or remotely.
|
||||
// If remote just 'won', our recently enabled list just got cleared.
|
||||
final HashSet<String> allDeclined = new HashSet<String>();
|
||||
|
||||
final Set<String> newRemoteDeclined = global.getDeclinedEngineNames();
|
||||
final Set<String> oldLocalDeclined = config.declinedEngineNames;
|
||||
|
||||
allDeclined.addAll(newRemoteDeclined);
|
||||
allDeclined.addAll(oldLocalDeclined);
|
||||
|
||||
if (config.userSelectedEngines != null) {
|
||||
for (Entry<String, Boolean> selection : config.userSelectedEngines.entrySet()) {
|
||||
if (selection.getValue()) {
|
||||
allDeclined.remove(selection.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config.declinedEngineNames = allDeclined;
|
||||
if (config.declinedEngineNames.isEmpty()) {
|
||||
Logger.debug(LOG_TAG, "meta/global reported no declined engine names, and we have none declined locally.");
|
||||
} else {
|
||||
if (Logger.shouldLogVerbose(LOG_TAG)) {
|
||||
Logger.trace(LOG_TAG, "Persisting declined engine names '" +
|
||||
Utils.toCommaSeparatedString(config.declinedEngineNames) + "' from meta/global.");
|
||||
}
|
||||
}
|
||||
|
||||
config.persistToPrefs();
|
||||
advance();
|
||||
}
|
||||
@ -901,6 +935,27 @@ public class GlobalSession implements PrefsSource, HttpResponseObserver {
|
||||
resetStages(this.getSyncStagesByName(names));
|
||||
}
|
||||
|
||||
/**
|
||||
* Engines to explicitly mark as declined in a fresh meta/global record.
|
||||
* <p>
|
||||
* Returns an empty array if the user hasn't elected to customize data types,
|
||||
* or an array of engines that the user un-checked during customization.
|
||||
* <p>
|
||||
* Engines that Android Sync doesn't recognize are <b>not</b> included in
|
||||
* the returned array.
|
||||
*
|
||||
* @return a new JSONArray of engine names.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected JSONArray declinedEngineNames() {
|
||||
final JSONArray declined = new JSONArray();
|
||||
for (String engine : config.declinedEngineNames) {
|
||||
declined.add(engine);
|
||||
};
|
||||
|
||||
return declined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Engines to include in a fresh meta/global record.
|
||||
* <p>
|
||||
@ -983,6 +1038,10 @@ public class GlobalSession implements PrefsSource, HttpResponseObserver {
|
||||
metaGlobal.setStorageVersion(STORAGE_VERSION);
|
||||
metaGlobal.setEngines(engines);
|
||||
|
||||
// We assume that the config's declined engines have been updated
|
||||
// according to the user's selections.
|
||||
metaGlobal.setDeclinedEngineNames(this.declinedEngineNames());
|
||||
|
||||
return metaGlobal;
|
||||
}
|
||||
|
||||
|
@ -6,11 +6,13 @@ package org.mozilla.gecko.sync;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.parser.ParseException;
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
import org.mozilla.gecko.sync.MetaGlobalException.MetaGlobalMalformedSyncIDException;
|
||||
@ -27,6 +29,7 @@ public class MetaGlobal implements SyncStorageRequestDelegate {
|
||||
|
||||
// Fields.
|
||||
protected ExtendedJSONObject engines;
|
||||
protected JSONArray declined;
|
||||
protected Long storageVersion;
|
||||
protected String syncID;
|
||||
|
||||
@ -77,6 +80,7 @@ public class MetaGlobal implements SyncStorageRequestDelegate {
|
||||
json.put("storageVersion", storageVersion);
|
||||
json.put("engines", engines);
|
||||
json.put("syncID", syncID);
|
||||
json.put("declined", declined);
|
||||
return json;
|
||||
}
|
||||
|
||||
@ -93,14 +97,18 @@ public class MetaGlobal implements SyncStorageRequestDelegate {
|
||||
return record;
|
||||
}
|
||||
|
||||
public void setFromRecord(CryptoRecord record) throws IllegalStateException, IOException, ParseException, NonObjectJSONException {
|
||||
public void setFromRecord(CryptoRecord record) throws IllegalStateException, IOException, ParseException, NonObjectJSONException, NonArrayJSONException {
|
||||
if (record == null) {
|
||||
throw new IllegalArgumentException("Cannot set meta/global from null record");
|
||||
}
|
||||
Logger.debug(LOG_TAG, "meta/global is " + record.payload.toJSONString());
|
||||
this.storageVersion = (Long) record.payload.get("storageVersion");
|
||||
this.syncID = (String) record.payload.get("syncID");
|
||||
|
||||
setEngines(record.payload.getObject("engines"));
|
||||
|
||||
// Accepts null -- declined can be missing.
|
||||
setDeclinedEngineNames(record.payload.getArray("declined"));
|
||||
}
|
||||
|
||||
public Long getStorageVersion() {
|
||||
@ -115,6 +123,59 @@ public class MetaGlobal implements SyncStorageRequestDelegate {
|
||||
return engines;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void declineEngine(String engine) {
|
||||
if (this.declined == null) {
|
||||
JSONArray replacement = new JSONArray();
|
||||
replacement.add(engine);
|
||||
setDeclinedEngineNames(replacement);
|
||||
return;
|
||||
}
|
||||
|
||||
this.declined.add(engine);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void declineEngineNames(Collection<String> additional) {
|
||||
if (this.declined == null) {
|
||||
JSONArray replacement = new JSONArray();
|
||||
replacement.addAll(additional);
|
||||
setDeclinedEngineNames(replacement);
|
||||
return;
|
||||
}
|
||||
|
||||
for (String engine : additional) {
|
||||
if (!this.declined.contains(engine)) {
|
||||
this.declined.add(engine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setDeclinedEngineNames(JSONArray declined) {
|
||||
if (declined == null) {
|
||||
this.declined = new JSONArray();
|
||||
return;
|
||||
}
|
||||
this.declined = declined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the set of engines that we support (given as an argument)
|
||||
* but the user hasn't explicitly declined on another device.
|
||||
*
|
||||
* Can return the input if the user hasn't declined any engines.
|
||||
*/
|
||||
public Set<String> getNonDeclinedEngineNames(Set<String> supported) {
|
||||
if (this.declined == null ||
|
||||
this.declined.isEmpty()) {
|
||||
return supported;
|
||||
}
|
||||
|
||||
final Set<String> result = new HashSet<String>(supported);
|
||||
result.removeAll(this.declined);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void setEngines(ExtendedJSONObject engines) {
|
||||
if (engines == null) {
|
||||
engines = new ExtendedJSONObject();
|
||||
@ -196,6 +257,14 @@ public class MetaGlobal implements SyncStorageRequestDelegate {
|
||||
return new HashSet<String>(engines.keySet());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Set<String> getDeclinedEngineNames() {
|
||||
if (declined == null) {
|
||||
return null;
|
||||
}
|
||||
return new HashSet<String>(declined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the server settings and local settings match.
|
||||
* Throws a specific MetaGlobalException if that's not the case.
|
||||
|
@ -198,6 +198,7 @@ public class SyncConfiguration {
|
||||
* fresh meta/global record for upload.
|
||||
*/
|
||||
public Set<String> enabledEngineNames;
|
||||
public Set<String> declinedEngineNames = new HashSet<String>();
|
||||
|
||||
/**
|
||||
* Names of stages to sync <it>this sync</it>, or <code>null</code> to sync
|
||||
@ -248,6 +249,7 @@ public class SyncConfiguration {
|
||||
public static final String PREF_SYNC_ID = "syncID";
|
||||
|
||||
public static final String PREF_ENABLED_ENGINE_NAMES = "enabledEngineNames";
|
||||
public static final String PREF_DECLINED_ENGINE_NAMES = "declinedEngineNames";
|
||||
public static final String PREF_USER_SELECTED_ENGINES_TO_SYNC = "userSelectedEngines";
|
||||
public static final String PREF_USER_SELECTED_ENGINES_TO_SYNC_TIMESTAMP = "userSelectedEnginesTimestamp";
|
||||
|
||||
@ -311,27 +313,47 @@ public class SyncConfiguration {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the engine names that are enabled in meta/global.
|
||||
* Gets the engine names that are enabled, declined, or other (depending on pref) in meta/global.
|
||||
*
|
||||
* @param prefs
|
||||
* SharedPreferences that the engines are associated with.
|
||||
* @param pref
|
||||
* The preference name to use. E.g, PREF_ENABLED_ENGINE_NAMES.
|
||||
* @return Set<String> of the enabled engine names if they have been stored,
|
||||
* or null otherwise.
|
||||
*/
|
||||
public static Set<String> getEnabledEngineNames(SharedPreferences prefs) {
|
||||
String json = prefs.getString(PREF_ENABLED_ENGINE_NAMES, null);
|
||||
protected static Set<String> getEngineNamesFromPref(SharedPreferences prefs, String pref) {
|
||||
final String json = prefs.getString(pref, null);
|
||||
if (json == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
ExtendedJSONObject o = ExtendedJSONObject.parseJSONObject(json);
|
||||
final ExtendedJSONObject o = ExtendedJSONObject.parseJSONObject(json);
|
||||
return new HashSet<String>(o.keySet());
|
||||
} catch (Exception e) {
|
||||
// enabledEngineNames can be null.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of engine names that the user has enabled. If none
|
||||
* have been stored in prefs, <code>null</code> is returned.
|
||||
*/
|
||||
public static Set<String> getEnabledEngineNames(SharedPreferences prefs) {
|
||||
return getEngineNamesFromPref(prefs, PREF_ENABLED_ENGINE_NAMES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of engine names that the user has declined.
|
||||
*/
|
||||
public static Set<String> getDeclinedEngineNames(SharedPreferences prefs) {
|
||||
final Set<String> names = getEngineNamesFromPref(prefs, PREF_DECLINED_ENGINE_NAMES);
|
||||
if (names == null) {
|
||||
return new HashSet<String>();
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the engines whose sync states have been changed by the user through the
|
||||
* SelectEnginesActivity.
|
||||
@ -371,6 +393,9 @@ public class SyncConfiguration {
|
||||
/**
|
||||
* Store a Map of engines and their sync states to prefs.
|
||||
*
|
||||
* Any engine that's disabled in the input is also recorded
|
||||
* as a declined engine, overwriting the stored values.
|
||||
*
|
||||
* @param prefs
|
||||
* SharedPreferences that the engines are associated with.
|
||||
* @param selectedEngines
|
||||
@ -378,20 +403,33 @@ public class SyncConfiguration {
|
||||
*/
|
||||
public static void storeSelectedEnginesToPrefs(SharedPreferences prefs, Map<String, Boolean> selectedEngines) {
|
||||
ExtendedJSONObject jObj = new ExtendedJSONObject();
|
||||
HashSet<String> declined = new HashSet<String>();
|
||||
for (Entry<String, Boolean> e : selectedEngines.entrySet()) {
|
||||
jObj.put(e.getKey(), e.getValue());
|
||||
final Boolean enabled = e.getValue();
|
||||
final String engine = e.getKey();
|
||||
jObj.put(engine, enabled);
|
||||
if (!enabled) {
|
||||
declined.add(engine);
|
||||
}
|
||||
}
|
||||
|
||||
// Our history checkbox drives form history, too.
|
||||
// We don't need to do this for enablement: that's done at retrieval time.
|
||||
if (selectedEngines.containsKey("history") && !selectedEngines.get("history").booleanValue()) {
|
||||
declined.add("forms");
|
||||
}
|
||||
|
||||
String json = jObj.toJSONString();
|
||||
long currentTime = System.currentTimeMillis();
|
||||
Editor edit = prefs.edit();
|
||||
edit.putString(PREF_USER_SELECTED_ENGINES_TO_SYNC, json);
|
||||
edit.putString(PREF_DECLINED_ENGINE_NAMES, setToJSONObjectString(declined));
|
||||
edit.putLong(PREF_USER_SELECTED_ENGINES_TO_SYNC_TIMESTAMP, currentTime);
|
||||
Logger.error(LOG_TAG, "Storing user-selected engines at [" + currentTime + "].");
|
||||
edit.commit();
|
||||
}
|
||||
|
||||
public void loadFromPrefs(SharedPreferences prefs) {
|
||||
|
||||
if (prefs.contains(PREF_CLUSTER_URL)) {
|
||||
String u = prefs.getString(PREF_CLUSTER_URL, null);
|
||||
try {
|
||||
@ -406,6 +444,7 @@ public class SyncConfiguration {
|
||||
Logger.trace(LOG_TAG, "Set syncID from bundle: " + syncID);
|
||||
}
|
||||
enabledEngineNames = getEnabledEngineNames(prefs);
|
||||
declinedEngineNames = getDeclinedEngineNames(prefs);
|
||||
userSelectedEngines = getUserSelectedEngines(prefs);
|
||||
userSelectedEnginesTimestamp = prefs.getLong(PREF_USER_SELECTED_ENGINES_TO_SYNC_TIMESTAMP, 0);
|
||||
// We don't set crypto/keys here because we need the syncKeyBundle to decrypt the JSON
|
||||
@ -417,6 +456,14 @@ public class SyncConfiguration {
|
||||
this.persistToPrefs(this.getPrefs());
|
||||
}
|
||||
|
||||
private static String setToJSONObjectString(Set<String> set) {
|
||||
ExtendedJSONObject o = new ExtendedJSONObject();
|
||||
for (String name : set) {
|
||||
o.put(name, 0);
|
||||
}
|
||||
return o.toJSONString();
|
||||
}
|
||||
|
||||
public void persistToPrefs(SharedPreferences prefs) {
|
||||
Editor edit = prefs.edit();
|
||||
if (clusterURL == null) {
|
||||
@ -430,11 +477,12 @@ public class SyncConfiguration {
|
||||
if (enabledEngineNames == null) {
|
||||
edit.remove(PREF_ENABLED_ENGINE_NAMES);
|
||||
} else {
|
||||
ExtendedJSONObject o = new ExtendedJSONObject();
|
||||
for (String engineName : enabledEngineNames) {
|
||||
o.put(engineName, 0);
|
||||
}
|
||||
edit.putString(PREF_ENABLED_ENGINE_NAMES, o.toJSONString());
|
||||
edit.putString(PREF_ENABLED_ENGINE_NAMES, setToJSONObjectString(enabledEngineNames));
|
||||
}
|
||||
if (declinedEngineNames.isEmpty()) {
|
||||
edit.remove(PREF_DECLINED_ENGINE_NAMES);
|
||||
} else {
|
||||
edit.putString(PREF_DECLINED_ENGINE_NAMES, setToJSONObjectString(declinedEngineNames));
|
||||
}
|
||||
if (userSelectedEngines == null) {
|
||||
edit.remove(PREF_USER_SELECTED_ENGINES_TO_SYNC);
|
||||
|
@ -515,7 +515,9 @@ public abstract class ServerSyncStage extends AbstractSessionManagingSyncStage i
|
||||
if (!isEnabled) {
|
||||
// Engine has been disabled; update meta/global with engine removal for upload.
|
||||
session.removeEngineFromMetaGlobal(name);
|
||||
session.config.declinedEngineNames.add(name);
|
||||
} else {
|
||||
session.config.declinedEngineNames.remove(name);
|
||||
// Add engine with new syncID to meta/global for upload.
|
||||
String newSyncID = Utils.generateGuid();
|
||||
session.recordForMetaGlobalUpdate(name, new EngineSettings(newSyncID, this.getStorageVersion()));
|
||||
|
@ -25,6 +25,33 @@ public class TestSyncConfiguration extends AndroidSyncTestCase implements PrefsS
|
||||
return this.getApplicationContext().getSharedPreferences(name, mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that declined engines persist through prefs.
|
||||
*/
|
||||
public void testDeclinedEngineNames() {
|
||||
SyncConfiguration config = null;
|
||||
SharedPreferences prefs = getPrefs(TEST_PREFS_NAME, 0);
|
||||
|
||||
config = newSyncConfiguration();
|
||||
config.declinedEngineNames = new HashSet<String>();
|
||||
config.declinedEngineNames.add("test1");
|
||||
config.declinedEngineNames.add("test2");
|
||||
config.persistToPrefs();
|
||||
assertTrue(prefs.contains(SyncConfiguration.PREF_DECLINED_ENGINE_NAMES));
|
||||
config = newSyncConfiguration();
|
||||
Set<String> expected = new HashSet<String>();
|
||||
for (String name : new String[] { "test1", "test2" }) {
|
||||
expected.add(name);
|
||||
}
|
||||
assertEquals(expected, config.declinedEngineNames);
|
||||
|
||||
config.declinedEngineNames = null;
|
||||
config.persistToPrefs();
|
||||
assertFalse(prefs.contains(SyncConfiguration.PREF_DECLINED_ENGINE_NAMES));
|
||||
config = newSyncConfiguration();
|
||||
assertNull(config.declinedEngineNames);
|
||||
}
|
||||
|
||||
public void testEnabledEngineNames() {
|
||||
SyncConfiguration config = null;
|
||||
SharedPreferences prefs = getPrefs(TEST_PREFS_NAME, 0);
|
||||
|
@ -33,7 +33,6 @@ dropcam.com: did not receive HSTS header
|
||||
email.lookout.com: could not connect to host
|
||||
emailprivacytester.com: did not receive HSTS header
|
||||
encrypted.google.com: did not receive HSTS header (error ignored - included regardless)
|
||||
errors.zenpayroll.com: could not connect to host
|
||||
espra.com: could not connect to host
|
||||
fatzebra.com.au: did not receive HSTS header
|
||||
fj.simple.com: did not receive HSTS header
|
||||
@ -79,8 +78,6 @@ paypal.com: max-age too low: 14400
|
||||
payroll.xero.com: max-age too low: 3600
|
||||
platform.lookout.com: could not connect to host
|
||||
play.google.com: did not receive HSTS header (error ignored - included regardless)
|
||||
plus.google.com: did not receive HSTS header (error ignored - included regardless)
|
||||
plus.sandbox.google.com: did not receive HSTS header (error ignored - included regardless)
|
||||
prodpad.com: did not receive HSTS header
|
||||
profiles.google.com: did not receive HSTS header (error ignored - included regardless)
|
||||
rapidresearch.me: did not receive HSTS header
|
||||
|
@ -8,7 +8,7 @@
|
||||
/*****************************************************************************/
|
||||
|
||||
#include <stdint.h>
|
||||
const PRTime gPreloadListExpirationTime = INT64_C(1405167392459000);
|
||||
const PRTime gPreloadListExpirationTime = INT64_C(1405764354321000);
|
||||
|
||||
class nsSTSPreload
|
||||
{
|
||||
@ -153,11 +153,12 @@ static const nsSTSPreload kSTSPreloadList[] = {
|
||||
{ "piratenlogin.de", true },
|
||||
{ "pixi.me", true },
|
||||
{ "play.google.com", false },
|
||||
{ "plus.google.com", true },
|
||||
{ "plus.sandbox.google.com", true },
|
||||
{ "plus.google.com", false },
|
||||
{ "plus.sandbox.google.com", false },
|
||||
{ "profiles.google.com", true },
|
||||
{ "publications.qld.gov.au", false },
|
||||
{ "riseup.net", true },
|
||||
{ "roddis.net", false },
|
||||
{ "romab.com", true },
|
||||
{ "roundcube.mayfirst.org", false },
|
||||
{ "sandbox.mydigipass.com", false },
|
||||
@ -211,6 +212,7 @@ static const nsSTSPreload kSTSPreloadList[] = {
|
||||
{ "www.mylookout.com", false },
|
||||
{ "www.noisebridge.net", false },
|
||||
{ "www.opsmate.com", true },
|
||||
{ "www.roddis.net", false },
|
||||
{ "www.simbolo.co.uk", false },
|
||||
{ "www.simple.com", false },
|
||||
{ "www.therapynotes.com", false },
|
||||
|
@ -139,7 +139,8 @@
|
||||
ALLOW_SYSCALL(epoll_ctl), \
|
||||
ALLOW_SYSCALL(sched_yield), \
|
||||
ALLOW_SYSCALL(sched_getscheduler), \
|
||||
ALLOW_SYSCALL(sched_setscheduler),
|
||||
ALLOW_SYSCALL(sched_setscheduler), \
|
||||
ALLOW_SYSCALL(sigaltstack),
|
||||
|
||||
#else
|
||||
#define SECCOMP_WHITELIST_B2G_HIGH
|
||||
|
@ -487,6 +487,12 @@ FxAccountsInternal.prototype = {
|
||||
if (!currentState.whenKeysReadyDeferred) {
|
||||
currentState.whenKeysReadyDeferred = Promise.defer();
|
||||
this.fetchAndUnwrapKeys(data.keyFetchToken).then(data => {
|
||||
if (!data.kA || !data.kB) {
|
||||
currentState.whenKeysReadyDeferred.reject(
|
||||
new Error("user data missing kA or kB")
|
||||
);
|
||||
return;
|
||||
}
|
||||
currentState.whenKeysReadyDeferred.resolve(data);
|
||||
});
|
||||
}
|
||||
|
@ -21,6 +21,17 @@ const SECOND_MS = 1000;
|
||||
const MINUTE_MS = SECOND_MS * 60;
|
||||
const HOUR_MS = MINUTE_MS * 60;
|
||||
|
||||
// This shouldn't be here - it should be part of the xpcshell harness.
|
||||
// Maybe as Assert.rejects - so we name it like that.
|
||||
function Assert_rejects(promise, message) {
|
||||
let deferred = Promise.defer();
|
||||
promise.then(
|
||||
() => deferred.reject(message || "Expected the promise to be rejected"),
|
||||
deferred.resolve
|
||||
);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
let identityConfig = makeIdentityConfig();
|
||||
let browseridManager = new BrowserIDManager();
|
||||
configureFxAccountIdentity(browseridManager, identityConfig);
|
||||
@ -254,20 +265,13 @@ add_task(function test_ensureLoggedIn() {
|
||||
Assert.ok(!browseridManager._shouldHaveSyncKeyBundle,
|
||||
"_shouldHaveSyncKeyBundle should be false so we know we are testing what we think we are.");
|
||||
Status.login = LOGIN_FAILED_NO_USERNAME;
|
||||
try {
|
||||
yield browseridManager.ensureLoggedIn();
|
||||
Assert.ok(false, "promise should have been rejected.")
|
||||
} catch(_) {
|
||||
}
|
||||
yield Assert_rejects(browseridManager.ensureLoggedIn(), "expecting rejection due to no user");
|
||||
Assert.ok(browseridManager._shouldHaveSyncKeyBundle,
|
||||
"_shouldHaveSyncKeyBundle should always be true after ensureLogin completes.");
|
||||
fxa.internal.currentAccountState.signedInUser = signedInUser;
|
||||
Status.login = LOGIN_FAILED_LOGIN_REJECTED;
|
||||
try {
|
||||
yield browseridManager.ensureLoggedIn();
|
||||
Assert.ok(false, "LOGIN_FAILED_LOGIN_REJECTED should have caused immediate rejection");
|
||||
} catch (_) {
|
||||
}
|
||||
yield Assert_rejects(browseridManager.ensureLoggedIn(),
|
||||
"LOGIN_FAILED_LOGIN_REJECTED should have caused immediate rejection");
|
||||
Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED,
|
||||
"status should remain LOGIN_FAILED_LOGIN_REJECTED");
|
||||
Status.login = LOGIN_FAILED_NETWORK_ERROR;
|
||||
@ -418,6 +422,52 @@ add_task(function test_getHAWKErrors() {
|
||||
Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR");
|
||||
});
|
||||
|
||||
add_task(function test_getKeysError() {
|
||||
_("BrowserIDManager correctly handles getKeys failures.");
|
||||
|
||||
let browseridManager = new BrowserIDManager();
|
||||
let identityConfig = makeIdentityConfig();
|
||||
// our mock identity config already has kA and kB - remove them or we never
|
||||
// try and fetch them.
|
||||
delete identityConfig.fxaccount.user.kA;
|
||||
delete identityConfig.fxaccount.user.kB;
|
||||
|
||||
configureFxAccountIdentity(browseridManager, identityConfig);
|
||||
|
||||
// Mock a fxAccounts object that returns no keys
|
||||
let fxa = new FxAccounts({
|
||||
fetchAndUnwrapKeys: function () {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
fxAccountsClient: new MockFxAccountsClient()
|
||||
});
|
||||
|
||||
// Add a mock to the currentAccountState object.
|
||||
fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) {
|
||||
this.cert = {
|
||||
validUntil: fxa.internal.now() + CERT_LIFETIME,
|
||||
cert: "certificate",
|
||||
};
|
||||
return Promise.resolve(this.cert.cert);
|
||||
};
|
||||
|
||||
// Ensure the new FxAccounts mock has a signed-in user.
|
||||
fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser;
|
||||
|
||||
browseridManager._fxaService = fxa;
|
||||
|
||||
yield browseridManager.initializeWithCurrentIdentity();
|
||||
|
||||
let ex;
|
||||
try {
|
||||
yield browseridManager.whenReadyToAuthenticate.promise;
|
||||
} catch (e) {
|
||||
ex = e;
|
||||
}
|
||||
|
||||
Assert.ok(ex.message.indexOf("missing kA or kB") >= 0);
|
||||
});
|
||||
|
||||
// End of tests
|
||||
// Utility functions follow
|
||||
|
||||
@ -455,10 +505,8 @@ function* initializeIdentityWithTokenServerFailure(response) {
|
||||
browseridManager._tokenServerClient = mockTSC;
|
||||
|
||||
yield browseridManager.initializeWithCurrentIdentity();
|
||||
try {
|
||||
yield browseridManager.whenReadyToAuthenticate.promise;
|
||||
Assert.ok(false, "expecting this promise to resolve with an error");
|
||||
} catch (ex) {}
|
||||
yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise,
|
||||
"expecting rejection due to tokenserver error");
|
||||
}
|
||||
|
||||
|
||||
@ -500,10 +548,8 @@ function* initializeIdentityWithHAWKFailure(response) {
|
||||
browseridManager._fxaService = fxa;
|
||||
browseridManager._signedInUser = null;
|
||||
yield browseridManager.initializeWithCurrentIdentity();
|
||||
try {
|
||||
yield browseridManager.whenReadyToAuthenticate.promise;
|
||||
Assert.ok(false, "expecting this promise to resolve with an error");
|
||||
} catch (ex) {}
|
||||
yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise,
|
||||
"expecting rejection due to hawk error");
|
||||
}
|
||||
|
||||
|
||||
|
@ -404,6 +404,14 @@ var LoginManagerContent = {
|
||||
return;
|
||||
}
|
||||
|
||||
// Somewhat gross hack - we don't want to show the "remember password"
|
||||
// notification on about:accounts for Firefox.
|
||||
let topWin = win.top;
|
||||
if (/^about:accounts($|\?)/i.test(topWin.document.documentURI)) {
|
||||
log("(form submission ignored -- about:accounts)");
|
||||
return;
|
||||
}
|
||||
|
||||
var formSubmitURL = LoginUtils._getActionOrigin(form)
|
||||
if (!Services.logins.getLoginSavingEnabled(hostname)) {
|
||||
log("(form submission ignored -- saving is disabled for:", hostname, ")");
|
||||
|
@ -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({
|
||||
|
@ -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
|
||||
|
102
toolkit/devtools/gcli/source/docs/design.md
Normal file
102
toolkit/devtools/gcli/source/docs/design.md
Normal 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.
|
213
toolkit/devtools/gcli/source/docs/developing-gcli.md
Normal file
213
toolkit/devtools/gcli/source/docs/developing-gcli.md
Normal 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.
|
||||
|
150
toolkit/devtools/gcli/source/docs/index.md
Normal file
150
toolkit/devtools/gcli/source/docs/index.md
Normal 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.
|
71
toolkit/devtools/gcli/source/docs/running-tests.md
Normal file
71
toolkit/devtools/gcli/source/docs/running-tests.md
Normal 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.
|
755
toolkit/devtools/gcli/source/docs/writing-commands.md
Normal file
755
toolkit/devtools/gcli/source/docs/writing-commands.md
Normal 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.
|
||||
|
20
toolkit/devtools/gcli/source/docs/writing-tests.md
Normal file
20
toolkit/devtools/gcli/source/docs/writing-tests.md
Normal 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
|
106
toolkit/devtools/gcli/source/docs/writing-types.md
Normal file
106
toolkit/devtools/gcli/source/docs/writing-types.md
Normal 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;
|
||||
};
|
183
toolkit/devtools/gcli/source/lib/gcli/connectors/index.js
Normal file
183
toolkit/devtools/gcli/source/lib/gcli/connectors/index.js
Normal 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);
|
||||
}
|
||||
});
|
||||
};
|
56
toolkit/devtools/gcli/source/lib/gcli/connectors/protocol.js
Normal file
56
toolkit/devtools/gcli/source/lib/gcli/connectors/protocol.js
Normal 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;
|
||||
};
|
147
toolkit/devtools/gcli/source/lib/gcli/connectors/rdp.js
Normal file
147
toolkit/devtools/gcli/source/lib/gcli/connectors/rdp.js
Normal 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
|
||||
});
|
||||
};
|
272
toolkit/devtools/gcli/source/lib/gcli/connectors/remoted.js
Normal file
272
toolkit/devtools/gcli/source/lib/gcli/connectors/remoted.js
Normal 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")
|
||||
})
|
||||
};
|
@ -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); }, []);
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
@ -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() { };
|
||||
|
@ -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
@ -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>
|
@ -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;
|
@ -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;
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
@ -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() { };
|
@ -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>
|
@ -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;
|
@ -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('');
|
||||
};
|
@ -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;
|
||||
};
|
@ -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;
|
||||
}
|
||||
};
|
@ -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;
|
Loading…
x
Reference in New Issue
Block a user