зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1667999 - [devtools] Land in-tree devtools-modules package. r=jdescottes
/!\ in order to take the new package in packages/ folder in account, you have to run `yarn` before `node bin/bundle.js` Differential Revision: https://phabricator.services.mozilla.com/D91732
This commit is contained in:
Родитель
d7ed96dfed
Коммит
eb3020eed5
|
@ -6,5 +6,6 @@ src/**/fixtures/**
|
|||
src/test/mochitest/**
|
||||
bin/
|
||||
packages/**/fixtures/**
|
||||
packages/devtools-modules/**
|
||||
node_modules
|
||||
out
|
||||
|
|
|
@ -684,7 +684,23 @@
|
|||
"external \"\\n(() => {\\n let factory;\\n function define(...args) {\\n if (factory) {\\n throw new Error(\\\"expected a single define call\\\");\\n }\\n\\n if (\\n args.length !== 2 ||\\n !Array.isArray(args[0]) ||\\n args[0].length !== 0 ||\\n typeof args[1] !== \\\"function\\\"\\n ) {\\n throw new Error(\\\"whatwg-url had unexpected factory arguments.\\\");\\n }\\n\\n factory = args[1];\\n }\\n define.amd = true;\\n\\n const existingDefine = Object.getOwnPropertyDescriptor(globalThis, \\\"define\\\");\\n globalThis.define = define;\\n let err;\\n try {\\n importScripts(\\\"resource://devtools/client/shared/vendor/whatwg-url.js\\\");\\n\\n if (!factory) {\\n throw new Error(\\\"Failed to load whatwg-url factory\\\");\\n }\\n } finally {\\n if (existingDefine) {\\n Object.defineProperty(globalThis, \\\"define\\\", existingDefine);\\n } else {\\n delete globalThis.define;\\n }\\n\\n }\\n\\n return factory();\\n})()\\n\"": 533,
|
||||
"../../@babel/types/lib/builders/flow/createFlowUnionType.js": 534,
|
||||
"../../@babel/types/lib/builders/typescript/createTSUnionType.js": 535,
|
||||
"../../@babel/types/lib/modifications/typescript/removeTypeDuplicates.js": 536
|
||||
"../../@babel/types/lib/modifications/typescript/removeTypeDuplicates.js": 536,
|
||||
"../../babel-loader/lib/index.js??ref--1!../../../packages/devtools-modules/src/utils/event-emitter.js": 537,
|
||||
"../../babel-loader/lib/index.js??ref--1!../../../packages/devtools-modules/index.js": 538,
|
||||
"../../../packages/devtools-modules/src/prefs.js": 539,
|
||||
"../../babel-loader/lib/index.js??ref--1!../../../packages/devtools-modules/src/utils/promise.js": 540,
|
||||
"../../babel-loader/lib/index.js??ref--1!../../../packages/devtools-modules/src/key-shortcuts.js": 541,
|
||||
"../../babel-loader/lib/index.js??ref--1!../../../packages/devtools-modules/src/zoom-keys.js": 542,
|
||||
"../../babel-loader/lib/index.js??ref--1!../../../packages/devtools-modules/src/async-storage.js": 543,
|
||||
"../../babel-loader/lib/index.js??ref--1!../../../packages/devtools-modules/src/source-utils.js": 544,
|
||||
"../../babel-loader/lib/index.js??ref--1!../../../packages/devtools-modules/src/utils/telemetry.js": 545,
|
||||
"../../babel-loader/lib/index.js??ref--1!../../../packages/devtools-modules/src/unicode-url.js": 546,
|
||||
"../../babel-loader/lib/index.js??ref--1!../../../packages/devtools-modules/src/plural-form.js": 547,
|
||||
"../../babel-loader/lib/index.js??ref--1!../../../packages/devtools-modules/src/saveAs.js": 548,
|
||||
"../../babel-loader/lib/index.js??ref--1!../../../packages/devtools-modules/src/async-store-helper.js": 549,
|
||||
"../../babel-loader/lib/index.js??ref--1!../../../packages/devtools-config/index.js": 550,
|
||||
"../../babel-loader/lib/index.js??ref--1!../../../packages/devtools-config/src/feature.js": 551,
|
||||
"../../babel-loader/lib/index.js??ref--1!../../../packages/devtools-environment/index.js": 552
|
||||
},
|
||||
"usedIds": {
|
||||
"0": 0,
|
||||
|
@ -1223,7 +1239,23 @@
|
|||
"533": 533,
|
||||
"534": 534,
|
||||
"535": 535,
|
||||
"536": 536
|
||||
"536": 536,
|
||||
"537": 537,
|
||||
"538": 538,
|
||||
"539": 539,
|
||||
"540": 540,
|
||||
"541": 541,
|
||||
"542": 542,
|
||||
"543": 543,
|
||||
"544": 544,
|
||||
"545": 545,
|
||||
"546": 546,
|
||||
"547": 547,
|
||||
"548": 548,
|
||||
"549": 549,
|
||||
"550": 550,
|
||||
"551": 551,
|
||||
"552": 552
|
||||
}
|
||||
},
|
||||
"chunks": {
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,362 @@
|
|||
Mozilla Public License, version 2.0
|
||||
|
||||
1. Definitions
|
||||
|
||||
1.1. "Contributor"
|
||||
|
||||
means each individual or legal entity that creates, contributes to the
|
||||
creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
|
||||
means the combination of the Contributions of others (if any) used by a
|
||||
Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
|
||||
means Source Code Form to which the initial Contributor has attached the
|
||||
notice in Exhibit A, the Executable Form of such Source Code Form, and
|
||||
Modifications of such Source Code Form, in each case including portions
|
||||
thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
a. that the initial Contributor has attached the notice described in
|
||||
Exhibit B to the Covered Software; or
|
||||
|
||||
b. that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the terms of
|
||||
a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
|
||||
means a work that combines Covered Software with other material, in a
|
||||
separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
|
||||
means having the right to grant, to the maximum extent possible, whether
|
||||
at the time of the initial grant or subsequently, any and all of the
|
||||
rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
|
||||
means any of the following:
|
||||
|
||||
a. any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered Software; or
|
||||
|
||||
b. any new file in Source Code Form that contains any Covered Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the License,
|
||||
by the making, using, selling, offering for sale, having made, import,
|
||||
or transfer of either its Contributions or its Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
|
||||
means either the GNU General Public License, Version 2.0, the GNU Lesser
|
||||
General Public License, Version 2.1, the GNU Affero General Public
|
||||
License, Version 3.0, or any later versions of those licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that controls, is
|
||||
controlled by, or is under common control with You. For purposes of this
|
||||
definition, "control" means (a) the power, direct or indirect, to cause
|
||||
the direction or management of such entity, whether by contract or
|
||||
otherwise, or (b) ownership of more than fifty percent (50%) of the
|
||||
outstanding shares or beneficial ownership of such entity.
|
||||
|
||||
|
||||
2. License Grants and Conditions
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
a. under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
b. under Patent Claims of such Contributor to make, use, sell, offer for
|
||||
sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
a. for any code that a Contributor has removed from Covered Software; or
|
||||
|
||||
b. for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
c. under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights to
|
||||
grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
|
||||
Section 2.1.
|
||||
|
||||
|
||||
3. Responsibilities
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
a. such Covered Software must also be made available in Source Code Form,
|
||||
as described in Section 3.1, and You must inform recipients of the
|
||||
Executable Form how they can obtain a copy of such Source Code Form by
|
||||
reasonable means in a timely manner, at a charge no more than the cost
|
||||
of distribution to the recipient; and
|
||||
|
||||
b. You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter the
|
||||
recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty, or
|
||||
limitations of liability) contained within the Source Code Form of the
|
||||
Covered Software, except that You may alter any license notices to the
|
||||
extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this License
|
||||
with respect to some or all of the Covered Software due to statute,
|
||||
judicial order, or regulation then You must: (a) comply with the terms of
|
||||
this License to the maximum extent possible; and (b) describe the
|
||||
limitations and the code they affect. Such description must be placed in a
|
||||
text file included with all distributions of the Covered Software under
|
||||
this License. Except to the extent prohibited by statute or regulation,
|
||||
such description must be sufficiently detailed for a recipient of ordinary
|
||||
skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically if You
|
||||
fail to comply with any of its terms. However, if You become compliant,
|
||||
then the rights granted under this License from a particular Contributor
|
||||
are reinstated (a) provisionally, unless and until such Contributor
|
||||
explicitly and finally terminates Your grants, and (b) on an ongoing
|
||||
basis, if such Contributor fails to notify You of the non-compliance by
|
||||
some reasonable means prior to 60 days after You have come back into
|
||||
compliance. Moreover, Your grants from a particular Contributor are
|
||||
reinstated on an ongoing basis if such Contributor notifies You of the
|
||||
non-compliance by some reasonable means, this is the first time You have
|
||||
received notice of non-compliance with this License from such
|
||||
Contributor, and You become compliant prior to 30 days after Your receipt
|
||||
of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
|
||||
license agreements (excluding distributors and resellers) which have been
|
||||
validly granted by You or Your distributors under this License prior to
|
||||
termination shall survive termination.
|
||||
|
||||
6. Disclaimer of Warranty
|
||||
|
||||
Covered Software is provided under this License on an "as is" basis,
|
||||
without warranty of any kind, either expressed, implied, or statutory,
|
||||
including, without limitation, warranties that the Covered Software is free
|
||||
of defects, merchantable, fit for a particular purpose or non-infringing.
|
||||
The entire risk as to the quality and performance of the Covered Software
|
||||
is with You. Should any Covered Software prove defective in any respect,
|
||||
You (not any Contributor) assume the cost of any necessary servicing,
|
||||
repair, or correction. This disclaimer of warranty constitutes an essential
|
||||
part of this License. No use of any Covered Software is authorized under
|
||||
this License except under this disclaimer.
|
||||
|
||||
7. Limitation of Liability
|
||||
|
||||
Under no circumstances and under no legal theory, whether tort (including
|
||||
negligence), contract, or otherwise, shall any Contributor, or anyone who
|
||||
distributes Covered Software as permitted above, be liable to You for any
|
||||
direct, indirect, special, incidental, or consequential damages of any
|
||||
character including, without limitation, damages for lost profits, loss of
|
||||
goodwill, work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses, even if such party shall have been
|
||||
informed of the possibility of such damages. This limitation of liability
|
||||
shall not apply to liability for death or personal injury resulting from
|
||||
such party's negligence to the extent applicable law prohibits such
|
||||
limitation. Some jurisdictions do not allow the exclusion or limitation of
|
||||
incidental or consequential damages, so this exclusion and limitation may
|
||||
not apply to You.
|
||||
|
||||
8. Litigation
|
||||
|
||||
Any litigation relating to this License may be brought only in the courts
|
||||
of a jurisdiction where the defendant maintains its principal place of
|
||||
business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions. Nothing
|
||||
in this Section shall prevent a party's ability to bring cross-claims or
|
||||
counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides that
|
||||
the language of a contract shall be construed against the drafter shall not
|
||||
be used to construe this License against a Contributor.
|
||||
|
||||
|
||||
10. Versions of the License
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses If You choose to distribute Source Code Form that is
|
||||
Incompatible With Secondary Licenses under the terms of this version of
|
||||
the License, the notice described in Exhibit B of this License must be
|
||||
attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular file,
|
||||
then You may include the notice in a location (such as a LICENSE file in a
|
||||
relevant directory) where a recipient would be likely to look for such a
|
||||
notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
|
||||
This Source Code Form is "Incompatible
|
||||
With Secondary Licenses", as defined by
|
||||
the Mozilla Public License, v. 2.0.
|
|
@ -0,0 +1,8 @@
|
|||
## Devtools Modules
|
||||
|
||||
[![Npm version](https://img.shields.io/npm/v/devtools-modules.svg)](https://npmjs.org/package/devtools-modules)
|
||||
|
||||
* *KeyShortcuts* - keyboard shortcuts library
|
||||
* *Menu* - Context Menu library
|
||||
* *Services* - A de-privilidged shim for prefs and appInfo
|
||||
* *PrefsHelper* - A Prefs convenience Wrapper
|
|
@ -0,0 +1,32 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { PrefsHelper } = require("./src/prefs");
|
||||
const KeyShortcuts = require("./src/key-shortcuts");
|
||||
const { ZoomKeys } = require("./src/zoom-keys");
|
||||
const EventEmitter = require("./src/utils/event-emitter");
|
||||
const asyncStorage = require("./src/async-storage");
|
||||
const asyncStoreHelper = require("./src/async-store-helper");
|
||||
const SourceUtils = require("./src/source-utils");
|
||||
const Telemetry = require("./src/utils/telemetry");
|
||||
const { getUnicodeHostname, getUnicodeUrlPath, getUnicodeUrl } =
|
||||
require("./src/unicode-url");
|
||||
const PluralForm = require("./src/plural-form");
|
||||
const saveAs = require("./src/saveAs")
|
||||
|
||||
module.exports = {
|
||||
KeyShortcuts,
|
||||
PrefsHelper,
|
||||
ZoomKeys,
|
||||
asyncStorage,
|
||||
asyncStoreHelper,
|
||||
EventEmitter,
|
||||
SourceUtils,
|
||||
Telemetry,
|
||||
getUnicodeHostname,
|
||||
getUnicodeUrlPath,
|
||||
getUnicodeUrl,
|
||||
PluralForm,
|
||||
saveAs
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "devtools-modules",
|
||||
"version": "1.1.9",
|
||||
"description": "DevTools Modules from M-C",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"license-check": "devtools-license-check",
|
||||
"test": "jest"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"punycode": "^2.1.0",
|
||||
"devtools-services": "0.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^23.0.0"
|
||||
},
|
||||
"files": [
|
||||
"src"
|
||||
],
|
||||
"jest": {
|
||||
"rootDir": "src",
|
||||
"testMatch": [
|
||||
"**/tests/**/*.js"
|
||||
],
|
||||
"testPathIgnorePatterns": [
|
||||
"<rootDir>/tests/helpers/local-storage-mock.js"
|
||||
],
|
||||
"transformIgnorePatterns": [],
|
||||
"setupFiles": [
|
||||
"<rootDir>/tests/helpers/local-storage-mock.js"
|
||||
],
|
||||
"moduleNameMapper": {},
|
||||
"testURL": "http://localhost/"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,214 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
|
||||
|
||||
/**
|
||||
*
|
||||
* Adapted from https://github.com/mozilla-b2g/gaia/blob/f09993563fb5fec4393eb71816ce76cb00463190/shared/js/async_storage.js
|
||||
* (converted to use Promises instead of callbacks).
|
||||
*
|
||||
* This file defines an asynchronous version of the localStorage API, backed by
|
||||
* an IndexedDB database. It creates a global asyncStorage object that has
|
||||
* methods like the localStorage object.
|
||||
*
|
||||
* To store a value use setItem:
|
||||
*
|
||||
* asyncStorage.setItem("key", "value");
|
||||
*
|
||||
* This returns a promise in case you want confirmation that the value has been stored.
|
||||
*
|
||||
* asyncStorage.setItem("key", "newvalue").then(function() {
|
||||
* console.log("new value stored");
|
||||
* });
|
||||
*
|
||||
* To read a value, call getItem(), but note that you must wait for a promise
|
||||
* resolution for the value to be retrieved.
|
||||
*
|
||||
* asyncStorage.getItem("key").then(function(value) {
|
||||
* console.log("The value of key is:", value);
|
||||
* });
|
||||
*
|
||||
* Note that unlike localStorage, asyncStorage does not allow you to store and
|
||||
* retrieve values by setting and querying properties directly. You cannot just
|
||||
* write asyncStorage.key; you have to explicitly call setItem() or getItem().
|
||||
*
|
||||
* removeItem(), clear(), length(), and key() are like the same-named methods of
|
||||
* localStorage, and all return a promise.
|
||||
*
|
||||
* The asynchronous nature of getItem() makes it tricky to retrieve multiple
|
||||
* values. But unlike localStorage, asyncStorage does not require the values you
|
||||
* store to be strings. So if you need to save multiple values and want to
|
||||
* retrieve them together, in a single asynchronous operation, just group the
|
||||
* values into a single object. The properties of this object may not include
|
||||
* DOM elements, but they may include things like Blobs and typed arrays.
|
||||
*
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const DBNAME = "devtools-async-storage";
|
||||
const DBVERSION = 1;
|
||||
const STORENAME = "keyvaluepairs";
|
||||
var db = null;
|
||||
|
||||
function withStore(type, onsuccess, onerror) {
|
||||
if (db) {
|
||||
const transaction = db.transaction(STORENAME, type);
|
||||
const store = transaction.objectStore(STORENAME);
|
||||
onsuccess(store);
|
||||
} else {
|
||||
const openreq = indexedDB.open(DBNAME, DBVERSION);
|
||||
openreq.onerror = function withStoreOnError() {
|
||||
onerror();
|
||||
};
|
||||
openreq.onupgradeneeded = function withStoreOnUpgradeNeeded() {
|
||||
// First time setup: create an empty object store
|
||||
openreq.result.createObjectStore(STORENAME);
|
||||
};
|
||||
openreq.onsuccess = function withStoreOnSuccess() {
|
||||
db = openreq.result;
|
||||
const transaction = db.transaction(STORENAME, type);
|
||||
const store = transaction.objectStore(STORENAME);
|
||||
onsuccess(store);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getItem(itemKey) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let req;
|
||||
withStore(
|
||||
"readonly",
|
||||
store => {
|
||||
store.transaction.oncomplete = function onComplete() {
|
||||
let value = req.result;
|
||||
if (value === undefined) {
|
||||
value = null;
|
||||
}
|
||||
resolve(value);
|
||||
};
|
||||
req = store.get(itemKey);
|
||||
req.onerror = function getItemOnError() {
|
||||
reject("Error in asyncStorage.getItem(): ", req.error.name);
|
||||
};
|
||||
},
|
||||
reject
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function setItem(itemKey, value) {
|
||||
return new Promise((resolve, reject) => {
|
||||
withStore(
|
||||
"readwrite",
|
||||
store => {
|
||||
store.transaction.oncomplete = resolve;
|
||||
const req = store.put(value, itemKey);
|
||||
req.onerror = function setItemOnError() {
|
||||
reject("Error in asyncStorage.setItem(): ", req.error.name);
|
||||
};
|
||||
},
|
||||
reject
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function removeItem(itemKey) {
|
||||
return new Promise((resolve, reject) => {
|
||||
withStore(
|
||||
"readwrite",
|
||||
store => {
|
||||
store.transaction.oncomplete = resolve;
|
||||
const req = store.delete(itemKey);
|
||||
req.onerror = function removeItemOnError() {
|
||||
reject("Error in asyncStorage.removeItem(): ", req.error.name);
|
||||
};
|
||||
},
|
||||
reject
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function clear() {
|
||||
return new Promise((resolve, reject) => {
|
||||
withStore(
|
||||
"readwrite",
|
||||
store => {
|
||||
store.transaction.oncomplete = resolve;
|
||||
const req = store.clear();
|
||||
req.onerror = function clearOnError() {
|
||||
reject("Error in asyncStorage.clear(): ", req.error.name);
|
||||
};
|
||||
},
|
||||
reject
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function length() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let req;
|
||||
withStore(
|
||||
"readonly",
|
||||
store => {
|
||||
store.transaction.oncomplete = function onComplete() {
|
||||
resolve(req.result);
|
||||
};
|
||||
req = store.count();
|
||||
req.onerror = function lengthOnError() {
|
||||
reject("Error in asyncStorage.length(): ", req.error.name);
|
||||
};
|
||||
},
|
||||
reject
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function key(n) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (n < 0) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let req;
|
||||
withStore(
|
||||
"readonly",
|
||||
store => {
|
||||
store.transaction.oncomplete = function onComplete() {
|
||||
const cursor = req.result;
|
||||
resolve(cursor ? cursor.key : null);
|
||||
};
|
||||
let advanced = false;
|
||||
req = store.openCursor();
|
||||
req.onsuccess = function keyOnSuccess() {
|
||||
const cursor = req.result;
|
||||
if (!cursor) {
|
||||
// this means there weren"t enough keys
|
||||
return;
|
||||
}
|
||||
if (n === 0 || advanced) {
|
||||
// Either 1) we have the first key, return it if that's what they
|
||||
// wanted, or 2) we"ve got the nth key.
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, ask the cursor to skip ahead n records
|
||||
advanced = true;
|
||||
cursor.advance(n);
|
||||
};
|
||||
req.onerror = function keyOnError() {
|
||||
reject("Error in asyncStorage.key(): ", req.error.name);
|
||||
};
|
||||
},
|
||||
reject
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
exports.getItem = getItem;
|
||||
exports.setItem = setItem;
|
||||
exports.removeItem = removeItem;
|
||||
exports.clear = clear;
|
||||
exports.length = length;
|
||||
exports.key = key;
|
|
@ -0,0 +1,57 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const asyncStorage = require("./async-storage");
|
||||
|
||||
/*
|
||||
* asyncStoreHelper wraps asyncStorage so that it is easy to define project
|
||||
* specific properties. It is similar to PrefsHelper.
|
||||
*
|
||||
* e.g.
|
||||
* const asyncStore = asyncStoreHelper("r", {a: "_a"})
|
||||
* asyncStore.a // => asyncStorage.getItem("r._a")
|
||||
* asyncStore.a = 2 // => asyncStorage.setItem("r._a", 2)
|
||||
*/
|
||||
function asyncStoreHelper(root, mappings) {
|
||||
let store = {};
|
||||
|
||||
function getMappingKey(key) {
|
||||
return Array.isArray(mappings[key]) ? mappings[key][0] : mappings[key];
|
||||
}
|
||||
|
||||
function getMappingDefaultValue(key) {
|
||||
return Array.isArray(mappings[key]) ? mappings[key][1] : null;
|
||||
}
|
||||
|
||||
Object.keys(mappings).map(key =>
|
||||
Object.defineProperty(store, key, {
|
||||
async get() {
|
||||
const value = await asyncStorage.getItem(
|
||||
`${root}.${getMappingKey(key)}`
|
||||
);
|
||||
return value || getMappingDefaultValue(key);
|
||||
},
|
||||
set(value) {
|
||||
return asyncStorage.setItem(`${root}.${getMappingKey(key)}`, value);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
store = new Proxy(store, {
|
||||
set: function(target, property, value, receiver) {
|
||||
if (!mappings.hasOwnProperty(property)) {
|
||||
throw new Error(`AsyncStore: ${property} is not defined in mappings`);
|
||||
}
|
||||
|
||||
Reflect.set(...arguments);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
module.exports = asyncStoreHelper;
|
|
@ -0,0 +1,243 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { appinfo } = require("devtools-services");
|
||||
const EventEmitter = require("./utils/event-emitter");
|
||||
const isOSX = appinfo.OS === "Darwin";
|
||||
|
||||
// List of electron keys mapped to DOM API (DOM_VK_*) key code
|
||||
const ElectronKeysMapping = {
|
||||
"F1": "DOM_VK_F1",
|
||||
"F2": "DOM_VK_F2",
|
||||
"F3": "DOM_VK_F3",
|
||||
"F4": "DOM_VK_F4",
|
||||
"F5": "DOM_VK_F5",
|
||||
"F6": "DOM_VK_F6",
|
||||
"F7": "DOM_VK_F7",
|
||||
"F8": "DOM_VK_F8",
|
||||
"F9": "DOM_VK_F9",
|
||||
"F10": "DOM_VK_F10",
|
||||
"F11": "DOM_VK_F11",
|
||||
"F12": "DOM_VK_F12",
|
||||
"F13": "DOM_VK_F13",
|
||||
"F14": "DOM_VK_F14",
|
||||
"F15": "DOM_VK_F15",
|
||||
"F16": "DOM_VK_F16",
|
||||
"F17": "DOM_VK_F17",
|
||||
"F18": "DOM_VK_F18",
|
||||
"F19": "DOM_VK_F19",
|
||||
"F20": "DOM_VK_F20",
|
||||
"F21": "DOM_VK_F21",
|
||||
"F22": "DOM_VK_F22",
|
||||
"F23": "DOM_VK_F23",
|
||||
"F24": "DOM_VK_F24",
|
||||
"Space": "DOM_VK_SPACE",
|
||||
"Backspace": "DOM_VK_BACK_SPACE",
|
||||
"Delete": "DOM_VK_DELETE",
|
||||
"Insert": "DOM_VK_INSERT",
|
||||
"Return": "DOM_VK_RETURN",
|
||||
"Enter": "DOM_VK_RETURN",
|
||||
"Up": "DOM_VK_UP",
|
||||
"Down": "DOM_VK_DOWN",
|
||||
"Left": "DOM_VK_LEFT",
|
||||
"Right": "DOM_VK_RIGHT",
|
||||
"Home": "DOM_VK_HOME",
|
||||
"End": "DOM_VK_END",
|
||||
"PageUp": "DOM_VK_PAGE_UP",
|
||||
"PageDown": "DOM_VK_PAGE_DOWN",
|
||||
"Escape": "DOM_VK_ESCAPE",
|
||||
"Esc": "DOM_VK_ESCAPE",
|
||||
"Tab": "DOM_VK_TAB",
|
||||
"VolumeUp": "DOM_VK_VOLUME_UP",
|
||||
"VolumeDown": "DOM_VK_VOLUME_DOWN",
|
||||
"VolumeMute": "DOM_VK_VOLUME_MUTE",
|
||||
"PrintScreen": "DOM_VK_PRINTSCREEN",
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to listen for keyboard events decribed in .properties file.
|
||||
*
|
||||
* let shortcuts = new KeyShortcuts({
|
||||
* window
|
||||
* });
|
||||
* shortcuts.on("Ctrl+F", event => {
|
||||
* // `event` is the KeyboardEvent which relates to the key shortcuts
|
||||
* });
|
||||
*
|
||||
* @param DOMWindow window
|
||||
* The window object of the document to listen events from.
|
||||
* @param DOMElement target
|
||||
* Optional DOM Element on which we should listen events from.
|
||||
* If omitted, we listen for all events fired on `window`.
|
||||
*/
|
||||
function KeyShortcuts({ window, target }) {
|
||||
this.window = window;
|
||||
this.target = target || window;
|
||||
this.keys = new Map();
|
||||
this.eventEmitter = new EventEmitter();
|
||||
this.target.addEventListener("keydown", this);
|
||||
}
|
||||
|
||||
/*
|
||||
* Parse an electron-like key string and return a normalized object which
|
||||
* allow efficient match on DOM key event. The normalized object matches DOM
|
||||
* API.
|
||||
*
|
||||
* @param DOMWindow window
|
||||
* Any DOM Window object, just to fetch its `KeyboardEvent` object
|
||||
* @param String str
|
||||
* The shortcut string to parse, following this document:
|
||||
* https://github.com/electron/electron/blob/master/docs/api/accelerator.md
|
||||
*/
|
||||
KeyShortcuts.parseElectronKey = function(window, str) {
|
||||
let modifiers = str.split("+");
|
||||
let key = modifiers.pop();
|
||||
|
||||
let shortcut = {
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
alt: false,
|
||||
shift: false,
|
||||
// Set for character keys
|
||||
key: undefined,
|
||||
// Set for non-character keys
|
||||
keyCode: undefined,
|
||||
};
|
||||
for (let mod of modifiers) {
|
||||
if (mod === "Alt") {
|
||||
shortcut.alt = true;
|
||||
} else if (["Command", "Cmd"].includes(mod)) {
|
||||
shortcut.meta = true;
|
||||
} else if (["CommandOrControl", "CmdOrCtrl"].includes(mod)) {
|
||||
if (isOSX) {
|
||||
shortcut.meta = true;
|
||||
} else {
|
||||
shortcut.ctrl = true;
|
||||
}
|
||||
} else if (["Control", "Ctrl"].includes(mod)) {
|
||||
shortcut.ctrl = true;
|
||||
} else if (mod === "Shift") {
|
||||
shortcut.shift = true;
|
||||
} else {
|
||||
console.error("Unsupported modifier:", mod, "from key:", str);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Plus is a special case. It's a character key and shouldn't be matched
|
||||
// against a keycode as it is only accessible via Shift/Capslock
|
||||
if (key === "Plus") {
|
||||
key = "+";
|
||||
}
|
||||
|
||||
if (typeof key === "string" && key.length === 1) {
|
||||
// Match any single character
|
||||
shortcut.key = key.toLowerCase();
|
||||
} else if (key in ElectronKeysMapping) {
|
||||
// Maps the others manually to DOM API DOM_VK_*
|
||||
key = ElectronKeysMapping[key];
|
||||
shortcut.keyCode = window.KeyboardEvent[key];
|
||||
// Used only to stringify the shortcut
|
||||
shortcut.keyCodeString = key;
|
||||
shortcut.key = key;
|
||||
} else {
|
||||
console.error("Unsupported key:", key);
|
||||
return null;
|
||||
}
|
||||
|
||||
return shortcut;
|
||||
};
|
||||
|
||||
KeyShortcuts.stringify = function(shortcut) {
|
||||
let list = [];
|
||||
if (shortcut.alt) {
|
||||
list.push("Alt");
|
||||
}
|
||||
if (shortcut.ctrl) {
|
||||
list.push("Ctrl");
|
||||
}
|
||||
if (shortcut.meta) {
|
||||
list.push("Cmd");
|
||||
}
|
||||
if (shortcut.shift) {
|
||||
list.push("Shift");
|
||||
}
|
||||
let key;
|
||||
if (shortcut.key) {
|
||||
key = shortcut.key.toUpperCase();
|
||||
} else {
|
||||
key = shortcut.keyCodeString;
|
||||
}
|
||||
list.push(key);
|
||||
return list.join("+");
|
||||
};
|
||||
|
||||
KeyShortcuts.prototype = {
|
||||
destroy() {
|
||||
this.target.removeEventListener("keydown", this);
|
||||
this.keys.clear();
|
||||
},
|
||||
|
||||
doesEventMatchShortcut(event, shortcut) {
|
||||
if (shortcut.meta != event.metaKey) {
|
||||
return false;
|
||||
}
|
||||
if (shortcut.ctrl != event.ctrlKey) {
|
||||
return false;
|
||||
}
|
||||
if (shortcut.alt != event.altKey) {
|
||||
return false;
|
||||
}
|
||||
// Shift is a special modifier, it may implicitely be required if the
|
||||
// expected key is a special character accessible via shift.
|
||||
if (shortcut.shift != event.shiftKey && event.key &&
|
||||
event.key.match(/[a-zA-Z]/)) {
|
||||
return false;
|
||||
}
|
||||
if (shortcut.keyCode) {
|
||||
return event.keyCode == shortcut.keyCode;
|
||||
} else if (event.key in ElectronKeysMapping) {
|
||||
return ElectronKeysMapping[event.key] === shortcut.key;
|
||||
}
|
||||
|
||||
// get the key from the keyCode if key is not provided.
|
||||
let key = event.key || String.fromCharCode(event.keyCode);
|
||||
|
||||
// For character keys, we match if the final character is the expected one.
|
||||
// But for digits we also accept indirect match to please azerty keyboard,
|
||||
// which requires Shift to be pressed to get digits.
|
||||
return key.toLowerCase() == shortcut.key ||
|
||||
(shortcut.key.match(/^[0-9]$/) &&
|
||||
event.keyCode == shortcut.key.charCodeAt(0));
|
||||
},
|
||||
|
||||
handleEvent(event) {
|
||||
for (let [key, shortcut] of this.keys) {
|
||||
if (this.doesEventMatchShortcut(event, shortcut)) {
|
||||
this.eventEmitter.emit(key, event);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
on(key, listener) {
|
||||
if (typeof listener !== "function") {
|
||||
throw new Error("KeyShortcuts.on() expects a function as " +
|
||||
"second argument");
|
||||
}
|
||||
if (!this.keys.has(key)) {
|
||||
let shortcut = KeyShortcuts.parseElectronKey(this.window, key);
|
||||
// The key string is wrong and we were unable to compute the key shortcut
|
||||
if (!shortcut) {
|
||||
return;
|
||||
}
|
||||
this.keys.set(key, shortcut);
|
||||
}
|
||||
this.eventEmitter.on(key, listener);
|
||||
},
|
||||
|
||||
off(key, listener) {
|
||||
this.eventEmitter.off(key, listener);
|
||||
},
|
||||
};
|
||||
module.exports = KeyShortcuts;
|
|
@ -0,0 +1,247 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Services from "devtools-services";
|
||||
|
||||
const { appinfo } = Services;
|
||||
|
||||
const isMacOS = appinfo.OS === "Darwin";
|
||||
|
||||
const EventEmitter = require("../utils/event-emitter");
|
||||
|
||||
/**
|
||||
* Formats key for use in tooltips
|
||||
* For macOS we use the following unicode
|
||||
*
|
||||
* cmd ⌘ = \u2318
|
||||
* shift ⇧ – \u21E7
|
||||
* option (alt) ⌥ \u2325
|
||||
*
|
||||
* For Win/Lin this replaces CommandOrControl or CmdOrCtrl with Ctrl
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
function formatKeyShortcut(shortcut) {
|
||||
if (isMacOS) {
|
||||
return shortcut
|
||||
.replace(/Shift\+/g, "\u21E7")
|
||||
.replace(/Command\+|Cmd\+/g, "\u2318")
|
||||
.replace(/CommandOrControl\+|CmdOrCtrl\+/g, "\u2318")
|
||||
.replace(/Alt\+/g, "\u2325");
|
||||
}
|
||||
return shortcut
|
||||
.replace(/CommandOrControl\+|CmdOrCtrl\+/g, `${L10N.getStr("ctrl")}+`)
|
||||
.replace(/Shift\+/g, "Shift+");
|
||||
}
|
||||
|
||||
function inToolbox() {
|
||||
try {
|
||||
return window.parent.document.documentURI.startsWith("about:devtools-toolbox");
|
||||
} catch (e) {
|
||||
// If `window` is not available, it's very likely that we are in the toolbox.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Copied from m-c DevToolsUtils.
|
||||
function getTopWindow(win) {
|
||||
return win.windowRoot ? win.windowRoot.ownerGlobal : win.top;
|
||||
}
|
||||
|
||||
/**
|
||||
* A partial implementation of the Menu API provided by electron:
|
||||
* https://github.com/electron/electron/blob/master/docs/api/menu.md.
|
||||
*
|
||||
* Extra features:
|
||||
* - Emits an 'open' and 'close' event when the menu is opened/closed
|
||||
|
||||
* @param String id (non standard)
|
||||
* Needed so tests can confirm the XUL implementation is working
|
||||
*/
|
||||
function Menu({ id = null } = {}) {
|
||||
this.menuitems = [];
|
||||
this.id = id;
|
||||
|
||||
Object.defineProperty(this, "items", {
|
||||
get() {
|
||||
return this.menuitems;
|
||||
}
|
||||
});
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an item to the end of the Menu
|
||||
*
|
||||
* @param {MenuItem} menuItem
|
||||
*/
|
||||
Menu.prototype.append = function (menuItem) {
|
||||
this.menuitems.push(menuItem);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add an item to a specified position in the menu
|
||||
*
|
||||
* @param {int} pos
|
||||
* @param {MenuItem} menuItem
|
||||
*/
|
||||
Menu.prototype.insert = function (pos, menuItem) {
|
||||
throw Error("Not implemented");
|
||||
};
|
||||
|
||||
/**
|
||||
* Show the Menu at a specified location on the screen
|
||||
*
|
||||
* Missing features:
|
||||
* - browserWindow - BrowserWindow (optional) - Default is null.
|
||||
* - positioningItem Number - (optional) OS X
|
||||
*
|
||||
* @param {int} screenX
|
||||
* @param {int} screenY
|
||||
* @param {Document} doc
|
||||
* The document that should own the context menu.
|
||||
*/
|
||||
Menu.prototype.popup = function (screenX, screenY, doc) {
|
||||
// The context-menu will be created in the topmost window to preserve keyboard
|
||||
// navigation. See Bug 1543940. Keep a reference on the window owning the menu to hide
|
||||
// the popup on unload.
|
||||
const win = doc.defaultView;
|
||||
doc = getTopWindow(doc.defaultView).document;
|
||||
|
||||
let popupset = doc.querySelector("popupset");
|
||||
if (!popupset) {
|
||||
popupset = doc.createXULElement("popupset");
|
||||
doc.documentElement.appendChild(popupset);
|
||||
}
|
||||
// See bug 1285229, on Windows, opening the same popup multiple times in a
|
||||
// row ends up duplicating the popup. The newly inserted popup doesn't
|
||||
// dismiss the old one. So remove any previously displayed popup before
|
||||
// opening a new one.
|
||||
let popup = popupset.querySelector("menupopup[menu-api=\"true\"]");
|
||||
if (popup) {
|
||||
popup.hidePopup();
|
||||
}
|
||||
|
||||
popup = this.createPopup(doc);
|
||||
popup.setAttribute("menu-api", "true");
|
||||
|
||||
if (this.id) {
|
||||
popup.id = this.id;
|
||||
}
|
||||
this._createMenuItems(popup);
|
||||
|
||||
// The context menu will be created in the topmost chrome window. Hide it manually when
|
||||
// the owner document is unloaded.
|
||||
const onWindowUnload = () => popup.hidePopup();
|
||||
win.addEventListener("unload", onWindowUnload);
|
||||
|
||||
// Remove the menu from the DOM once it's hidden.
|
||||
popup.addEventListener("popuphidden", (e) => {
|
||||
if (e.target === popup) {
|
||||
win.removeEventListener("unload", onWindowUnload);
|
||||
popup.remove();
|
||||
this.emit("close", popup);
|
||||
}
|
||||
});
|
||||
|
||||
popup.addEventListener("popupshown", (e) => {
|
||||
if (e.target === popup) {
|
||||
this.emit("open", popup);
|
||||
}
|
||||
});
|
||||
|
||||
popupset.appendChild(popup);
|
||||
popup.openPopupAtScreen(screenX, screenY, true);
|
||||
};
|
||||
|
||||
Menu.prototype.createPopup = function(doc) {
|
||||
return doc.createElement("menupopup");
|
||||
}
|
||||
|
||||
Menu.prototype._createMenuItems = function(parent) {
|
||||
let doc = parent.ownerDocument;
|
||||
this.menuitems.forEach(item => {
|
||||
if (!item.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.submenu) {
|
||||
let menupopup = doc.createElement("menupopup");
|
||||
item.submenu._createMenuItems(menupopup);
|
||||
|
||||
let menuitem = doc.createElement("menuitem");
|
||||
menuitem.setAttribute("label", item.label);
|
||||
if (!inToolbox()) {
|
||||
menuitem.textContent = item.label;
|
||||
}
|
||||
|
||||
let menu = doc.createElement("menu");
|
||||
menu.appendChild(menuitem);
|
||||
menu.appendChild(menupopup);
|
||||
if (item.disabled) {
|
||||
menu.setAttribute("disabled", "true");
|
||||
}
|
||||
if (item.accesskey) {
|
||||
menu.setAttribute("accesskey", item.accesskey);
|
||||
}
|
||||
if (item.id) {
|
||||
menu.id = item.id;
|
||||
}
|
||||
if (item.accelerator) {
|
||||
menuitem.setAttribute("acceltext", formatKeyShortcut(item.accelerator));
|
||||
}
|
||||
parent.appendChild(menu);
|
||||
} else if (item.type === "separator") {
|
||||
let menusep = doc.createElement("menuseparator");
|
||||
parent.appendChild(menusep);
|
||||
} else {
|
||||
let menuitem = doc.createElement("menuitem");
|
||||
menuitem.setAttribute("label", item.label);
|
||||
|
||||
if (!inToolbox()) {
|
||||
menuitem.textContent = item.label;
|
||||
}
|
||||
|
||||
menuitem.addEventListener("command", () => item.click());
|
||||
|
||||
if (item.type === "checkbox") {
|
||||
menuitem.setAttribute("type", "checkbox");
|
||||
}
|
||||
if (item.type === "radio") {
|
||||
menuitem.setAttribute("type", "radio");
|
||||
}
|
||||
if (item.disabled) {
|
||||
menuitem.setAttribute("disabled", "true");
|
||||
}
|
||||
if (item.checked) {
|
||||
menuitem.setAttribute("checked", "true");
|
||||
}
|
||||
if (item.accesskey) {
|
||||
menuitem.setAttribute("accesskey", item.accesskey);
|
||||
}
|
||||
if (item.id) {
|
||||
menuitem.id = item.id;
|
||||
}
|
||||
if (item.accelerator) {
|
||||
menuitem.setAttribute("acceltext", formatKeyShortcut(item.accelerator));
|
||||
}
|
||||
parent.appendChild(menuitem);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Menu.setApplicationMenu = () => {
|
||||
throw Error("Not implemented");
|
||||
};
|
||||
|
||||
Menu.sendActionToFirstResponder = () => {
|
||||
throw Error("Not implemented");
|
||||
};
|
||||
|
||||
Menu.buildFromTemplate = () => {
|
||||
throw Error("Not implemented");
|
||||
};
|
||||
|
||||
module.exports = Menu;
|
|
@ -0,0 +1,64 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/**
|
||||
* A partial implementation of the MenuItem API provided by electron:
|
||||
* https://github.com/electron/electron/blob/master/docs/api/menu-item.md.
|
||||
*
|
||||
* Missing features:
|
||||
* - id String - Unique within a single menu. If defined then it can be used
|
||||
* as a reference to this item by the position attribute.
|
||||
* - role String - Define the action of the menu item; when specified the
|
||||
* click property will be ignored
|
||||
* - sublabel String
|
||||
* - icon NativeImage
|
||||
* - position String - This field allows fine-grained definition of the
|
||||
* specific location within a given menu.
|
||||
*
|
||||
* Implemented features:
|
||||
* @param Object options
|
||||
* Function click
|
||||
* Will be called with click(menuItem, browserWindow) when the menu item
|
||||
* is clicked
|
||||
* String type
|
||||
* Can be normal, separator, submenu, checkbox or radio
|
||||
* String label
|
||||
* Boolean enabled
|
||||
* If false, the menu item will be greyed out and unclickable.
|
||||
* Boolean checked
|
||||
* Should only be specified for checkbox or radio type menu items.
|
||||
* Menu submenu
|
||||
* Should be specified for submenu type menu items. If submenu is specified,
|
||||
* the type: 'submenu' can be omitted. If the value is not a Menu then it
|
||||
* will be automatically converted to one using Menu.buildFromTemplate.
|
||||
* Boolean visible
|
||||
* If false, the menu item will be entirely hidden.
|
||||
* String accelerator
|
||||
* If specified, will be used as accelerator text for MenuItem
|
||||
*/
|
||||
function MenuItem({
|
||||
accesskey = null,
|
||||
checked = false,
|
||||
click = () => {},
|
||||
disabled = false,
|
||||
label = "",
|
||||
id = null,
|
||||
submenu = null,
|
||||
type = "normal",
|
||||
visible = true,
|
||||
accelerator = "",
|
||||
} = { }) {
|
||||
this.accesskey = accesskey;
|
||||
this.checked = checked;
|
||||
this.click = click;
|
||||
this.disabled = disabled;
|
||||
this.id = id;
|
||||
this.label = label;
|
||||
this.submenu = submenu;
|
||||
this.type = type;
|
||||
this.visible = visible;
|
||||
this.accelerator = accelerator;
|
||||
}
|
||||
|
||||
module.exports = MenuItem;
|
|
@ -0,0 +1,160 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// These are the available plural functions that give the appropriate index
|
||||
// based on the plural rule number specified. The first element is the number
|
||||
// of plural forms and the second is the function to figure out the index.
|
||||
const gFunctions = [
|
||||
// 0: Chinese
|
||||
[1, (n) => 0],
|
||||
// 1: English
|
||||
[2, (n) => n!=1?1:0],
|
||||
// 2: French
|
||||
[2, (n) => n>1?1:0],
|
||||
// 3: Latvian
|
||||
[3, (n) => n%10==1&&n%100!=11?1:n%10==0?0:2],
|
||||
// 4: Scottish Gaelic
|
||||
[4, (n) => n==1||n==11?0:n==2||n==12?1:n>0&&n<20?2:3],
|
||||
// 5: Romanian
|
||||
[3, (n) => n==1?0:n==0||n%100>0&&n%100<20?1:2],
|
||||
// 6: Lithuanian
|
||||
[3, (n) => n%10==1&&n%100!=11?0:n%10>=2&&(n%100<10||n%100>=20)?2:1],
|
||||
// 7: Russian
|
||||
[3, (n) => n%10==1&&n%100!=11?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2],
|
||||
// 8: Slovak
|
||||
[3, (n) => n==1?0:n>=2&&n<=4?1:2],
|
||||
// 9: Polish
|
||||
[3, (n) => n==1?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2],
|
||||
// 10: Slovenian
|
||||
[4, (n) => n%100==1?0:n%100==2?1:n%100==3||n%100==4?2:3],
|
||||
// 11: Irish Gaeilge
|
||||
[5, (n) => n==1?0:n==2?1:n>=3&&n<=6?2:n>=7&&n<=10?3:4],
|
||||
// 12: Arabic
|
||||
[6, (n) => n==0?5:n==1?0:n==2?1:n%100>=3&&n%100<=10?2:n%100>=11&&n%100<=99?3:4],
|
||||
// 13: Maltese
|
||||
[4, (n) => n==1?0:n==0||n%100>0&&n%100<=10?1:n%100>10&&n%100<20?2:3],
|
||||
// 14: Unused
|
||||
[3, (n) => n%10==1?0:n%10==2?1:2],
|
||||
// 15: Icelandic, Macedonian
|
||||
[2, (n) => n%10==1&&n%100!=11?0:1],
|
||||
// 16: Breton
|
||||
[5, (n) => n%10==1&&n%100!=11&&n%100!=71&&n%100!=91?0:n%10==2&&n%100!=12&&n%100!=72&&n%100!=92?1:(n%10==3||n%10==4||n%10==9)&&n%100!=13&&n%100!=14&&n%100!=19&&n%100!=73&&n%100!=74&&n%100!=79&&n%100!=93&&n%100!=94&&n%100!=99?2:n%1000000==0&&n!=0?3:4],
|
||||
// 17: Shuar
|
||||
[2, (n) => n!=0?1:0],
|
||||
// 18: Welsh
|
||||
[6, (n) => n==0?0:n==1?1:n==2?2:n==3?3:n==6?4:5],
|
||||
// 19: Bosnian, Croatian, Serbian
|
||||
[3, (n) => n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2],
|
||||
];
|
||||
|
||||
const PluralForm = {
|
||||
/**
|
||||
* Get the correct plural form of a word based on the number
|
||||
*
|
||||
* @param aNum
|
||||
* The number to decide which plural form to use
|
||||
* @param aWords
|
||||
* A semi-colon (;) separated string of words to pick the plural form
|
||||
* @return The appropriate plural form of the word
|
||||
*/
|
||||
get get()
|
||||
{
|
||||
// This method will lazily load to avoid perf when it is first needed and
|
||||
// creates getPluralForm function. The function it creates is based on the
|
||||
// value of pluralRule specified in the intl stringbundle.
|
||||
// See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
|
||||
|
||||
// Delete the getters to be overwritten
|
||||
delete this.numForms;
|
||||
delete this.get;
|
||||
|
||||
// Make the plural form get function and set it as the default get
|
||||
[this.get, this.numForms] = this.makeGetter(this.ruleNum);
|
||||
return this.get;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a pair of plural form functions for the given plural rule number.
|
||||
*
|
||||
* @param aRuleNum
|
||||
* The plural rule number to create functions
|
||||
* @return A pair: [function that gets the right plural form,
|
||||
* function that returns the number of plural forms]
|
||||
*/
|
||||
makeGetter: function(aRuleNum)
|
||||
{
|
||||
// Default to "all plural" if the value is out of bounds or invalid
|
||||
if (aRuleNum < 0 || aRuleNum >= gFunctions.length || isNaN(aRuleNum)) {
|
||||
log(["Invalid rule number: ", aRuleNum, " -- defaulting to 0"]);
|
||||
aRuleNum = 0;
|
||||
}
|
||||
|
||||
// Get the desired pluralRule function
|
||||
let [numForms, pluralFunc] = gFunctions[aRuleNum];
|
||||
|
||||
// Return functions that give 1) the number of forms and 2) gets the right
|
||||
// plural form
|
||||
return [function(aNum, aWords) {
|
||||
// Figure out which index to use for the semi-colon separated words
|
||||
let index = pluralFunc(aNum ? Number(aNum) : 0);
|
||||
let words = aWords ? aWords.split(/;/) : [""];
|
||||
|
||||
// Explicitly check bounds to avoid strict warnings
|
||||
let ret = index < words.length ? words[index] : undefined;
|
||||
|
||||
// Check for array out of bounds or empty strings
|
||||
if ((ret == undefined) || (ret == "")) {
|
||||
// Display a message in the error console
|
||||
log(["Index #", index, " of '", aWords, "' for value ", aNum,
|
||||
" is invalid -- plural rule #", aRuleNum, ";"]);
|
||||
|
||||
// Default to the first entry (which might be empty, but not undefined)
|
||||
ret = words[0];
|
||||
}
|
||||
|
||||
return ret;
|
||||
}, () => numForms];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the number of forms for the current plural rule
|
||||
*
|
||||
* @return The number of forms
|
||||
*/
|
||||
get numForms()
|
||||
{
|
||||
// We lazily load numForms, so trigger the init logic with get()
|
||||
this.get();
|
||||
return this.numForms;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the plural rule number from the intl stringbundle
|
||||
*
|
||||
* @return The plural rule number
|
||||
*/
|
||||
get ruleNum()
|
||||
{
|
||||
try {
|
||||
return parseInt(L10N.getStr("pluralRule"), 10);
|
||||
} catch (e) {
|
||||
// Fallback to English if the pluralRule property is not available.
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Private helper function to log errors to the error console and command line
|
||||
*
|
||||
* @param aMsg
|
||||
* Error message to log or an array of strings to concat
|
||||
*/
|
||||
function log(aMsg)
|
||||
{
|
||||
let msg = "plural-form.js: " + (aMsg.join ? aMsg.join("") : aMsg);
|
||||
console.log(msg + "\n");
|
||||
}
|
||||
|
||||
module.exports = PluralForm;
|
|
@ -0,0 +1,187 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const Services = require("devtools-services");
|
||||
const EventEmitter = require("./utils/event-emitter");
|
||||
|
||||
/**
|
||||
* Shortcuts for lazily accessing and setting various preferences.
|
||||
* Usage:
|
||||
* let prefs = new Prefs("root.path.to.branch", {
|
||||
* myIntPref: ["Int", "leaf.path.to.my-int-pref"],
|
||||
* myCharPref: ["Char", "leaf.path.to.my-char-pref"],
|
||||
* myJsonPref: ["Json", "leaf.path.to.my-json-pref"],
|
||||
* myFloatPref: ["Float", "leaf.path.to.my-float-pref"]
|
||||
* ...
|
||||
* });
|
||||
*
|
||||
* Get/set:
|
||||
* prefs.myCharPref = "foo";
|
||||
* let aux = prefs.myCharPref;
|
||||
*
|
||||
* Observe:
|
||||
* prefs.registerObserver();
|
||||
* prefs.on("pref-changed", (prefName, prefValue) => {
|
||||
* ...
|
||||
* });
|
||||
*
|
||||
* @param string prefsRoot
|
||||
* The root path to the required preferences branch.
|
||||
* @param object prefsBlueprint
|
||||
* An object containing { accessorName: [prefType, prefName, prefDefault] } keys.
|
||||
*/
|
||||
function PrefsHelper(prefsRoot = "", prefsBlueprint = {}) {
|
||||
EventEmitter.decorate(this);
|
||||
|
||||
let cache = new Map();
|
||||
|
||||
for (let accessorName in prefsBlueprint) {
|
||||
let [prefType, prefName, prefDefault] = prefsBlueprint[accessorName];
|
||||
map(this, cache, accessorName, prefType, prefsRoot, prefName, prefDefault);
|
||||
}
|
||||
|
||||
let observer = makeObserver(this, cache, prefsRoot, prefsBlueprint);
|
||||
this.registerObserver = () => observer.register();
|
||||
this.unregisterObserver = () => observer.unregister();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for getting a pref value.
|
||||
*
|
||||
* @param Map cache
|
||||
* @param string prefType
|
||||
* @param string prefsRoot
|
||||
* @param string prefName
|
||||
* @return any
|
||||
*/
|
||||
function get(cache, prefType, prefsRoot, prefName) {
|
||||
let cachedPref = cache.get(prefName);
|
||||
if (cachedPref !== undefined) {
|
||||
return cachedPref;
|
||||
}
|
||||
let value = Services.prefs["get" + prefType + "Pref"](
|
||||
[prefsRoot, prefName].join(".")
|
||||
);
|
||||
cache.set(prefName, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for setting a pref value.
|
||||
*
|
||||
* @param Map cache
|
||||
* @param string prefType
|
||||
* @param string prefsRoot
|
||||
* @param string prefName
|
||||
* @param any value
|
||||
*/
|
||||
function set(cache, prefType, prefsRoot, prefName, value) {
|
||||
Services.prefs["set" + prefType + "Pref"](
|
||||
[prefsRoot, prefName].join("."),
|
||||
value
|
||||
);
|
||||
cache.set(prefName, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a property name to a pref, defining lazy getters and setters.
|
||||
* Supported types are "Bool", "Char", "Int", "Float" (sugar around "Char"
|
||||
* type and casting), and "Json" (which is basically just sugar for "Char"
|
||||
* using the standard JSON serializer).
|
||||
*
|
||||
* @param PrefsHelper self
|
||||
* @param Map cache
|
||||
* @param string accessorName
|
||||
* @param string prefType
|
||||
* @param string prefsRoot
|
||||
* @param string prefName
|
||||
* @param string prefDefault
|
||||
* @param array serializer [optional]
|
||||
*/
|
||||
function map(self, cache, accessorName, prefType, prefsRoot, prefName, prefDefault,
|
||||
serializer = { in: e => e, out: e => e }) {
|
||||
if (prefName in self) {
|
||||
throw new Error(`Can't use ${prefName} because it overrides a property` +
|
||||
"on the instance.");
|
||||
}
|
||||
if (prefType == "Json") {
|
||||
map(self, cache, accessorName, "String", prefsRoot, prefName, prefDefault, {
|
||||
in: JSON.parse,
|
||||
out: JSON.stringify
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (prefType == "Float") {
|
||||
map(self, cache, accessorName, "Char", prefsRoot, prefName, prefDefault, {
|
||||
in: Number.parseFloat,
|
||||
out: (n) => n + ""
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Object.defineProperty(self, accessorName, {
|
||||
get: () => {
|
||||
try {
|
||||
return serializer.in(get(cache, prefType, prefsRoot, prefName));
|
||||
} catch (e) {
|
||||
if (typeof prefDefault !== 'undefined') {
|
||||
return prefDefault;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
set: (e) => set(cache, prefType, prefsRoot, prefName, serializer.out(e))
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the accessor for the provided pref, based on the blueprint object
|
||||
* used in the constructor.
|
||||
*
|
||||
* @param PrefsHelper self
|
||||
* @param object prefsBlueprint
|
||||
* @return string
|
||||
*/
|
||||
function accessorNameForPref(somePrefName, prefsBlueprint) {
|
||||
for (let accessorName in prefsBlueprint) {
|
||||
let [, prefName] = prefsBlueprint[accessorName];
|
||||
if (somePrefName == prefName) {
|
||||
return accessorName;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a pref observer for `self`.
|
||||
*
|
||||
* @param PrefsHelper self
|
||||
* @param Map cache
|
||||
* @param string prefsRoot
|
||||
* @param object prefsBlueprint
|
||||
* @return object
|
||||
*/
|
||||
function makeObserver(self, cache, prefsRoot, prefsBlueprint) {
|
||||
return {
|
||||
register: function() {
|
||||
this._branch = Services.prefs.getBranch(prefsRoot + ".");
|
||||
this._branch.addObserver("", this);
|
||||
},
|
||||
unregister: function() {
|
||||
this._branch.removeObserver("", this);
|
||||
},
|
||||
observe: function(subject, topic, prefName) {
|
||||
// If this particular pref isn't handled by the blueprint object,
|
||||
// even though it's in the specified branch, ignore it.
|
||||
let accessorName = accessorNameForPref(prefName, prefsBlueprint);
|
||||
if (!(accessorName in self)) {
|
||||
return;
|
||||
}
|
||||
cache.delete(prefName);
|
||||
self.emit("pref-changed", accessorName, self[accessorName]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
exports.PrefsHelper = PrefsHelper;
|
|
@ -0,0 +1,5 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
module.exports = () => {}
|
|
@ -0,0 +1,339 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// TODO : Localize this (was l10n.getStr("frame.unknownSource"))
|
||||
const UNKNOWN_SOURCE_STRING = "(unknown)";
|
||||
|
||||
// Character codes used in various parsing helper functions.
|
||||
const CHAR_CODE_A = "a".charCodeAt(0);
|
||||
const CHAR_CODE_B = "b".charCodeAt(0);
|
||||
const CHAR_CODE_C = "c".charCodeAt(0);
|
||||
const CHAR_CODE_D = "d".charCodeAt(0);
|
||||
const CHAR_CODE_E = "e".charCodeAt(0);
|
||||
const CHAR_CODE_F = "f".charCodeAt(0);
|
||||
const CHAR_CODE_H = "h".charCodeAt(0);
|
||||
const CHAR_CODE_I = "i".charCodeAt(0);
|
||||
const CHAR_CODE_J = "j".charCodeAt(0);
|
||||
const CHAR_CODE_L = "l".charCodeAt(0);
|
||||
const CHAR_CODE_M = "m".charCodeAt(0);
|
||||
const CHAR_CODE_N = "n".charCodeAt(0);
|
||||
const CHAR_CODE_O = "o".charCodeAt(0);
|
||||
const CHAR_CODE_P = "p".charCodeAt(0);
|
||||
const CHAR_CODE_R = "r".charCodeAt(0);
|
||||
const CHAR_CODE_S = "s".charCodeAt(0);
|
||||
const CHAR_CODE_T = "t".charCodeAt(0);
|
||||
const CHAR_CODE_U = "u".charCodeAt(0);
|
||||
const CHAR_CODE_W = "w".charCodeAt(0);
|
||||
const CHAR_CODE_COLON = ":".charCodeAt(0);
|
||||
const CHAR_CODE_DASH = "-".charCodeAt(0);
|
||||
const CHAR_CODE_L_SQUARE_BRACKET = "[".charCodeAt(0);
|
||||
const CHAR_CODE_SLASH = "/".charCodeAt(0);
|
||||
const CHAR_CODE_CAP_S = "S".charCodeAt(0);
|
||||
|
||||
// The cache used in the `parseURL` function.
|
||||
const gURLStore = new Map();
|
||||
// The cache used in the `getSourceNames` function.
|
||||
const gSourceNamesStore = new Map();
|
||||
|
||||
/**
|
||||
* Takes a string and returns an object containing all the properties
|
||||
* available on an URL instance, with additional properties (fileName),
|
||||
* Leverages caching.
|
||||
*
|
||||
* @param {String} location
|
||||
* @return {Object?} An object containing most properties available
|
||||
* in https://developer.mozilla.org/en-US/docs/Web/API/URL
|
||||
*/
|
||||
|
||||
function parseURL(location) {
|
||||
let url = gURLStore.get(location);
|
||||
|
||||
if (url !== void 0) {
|
||||
return url;
|
||||
}
|
||||
|
||||
try {
|
||||
url = new URL(location);
|
||||
// The callers were generally written to expect a URL from
|
||||
// sdk/url, which is subtly different. So, work around some
|
||||
// important differences here.
|
||||
url = {
|
||||
href: url.href,
|
||||
protocol: url.protocol,
|
||||
host: url.host,
|
||||
hostname: url.hostname,
|
||||
port: url.port || null,
|
||||
pathname: url.pathname,
|
||||
search: url.search,
|
||||
hash: url.hash,
|
||||
username: url.username,
|
||||
password: url.password,
|
||||
origin: url.origin,
|
||||
};
|
||||
|
||||
// Definitions:
|
||||
// Example: https://foo.com:8888/file.js
|
||||
// `hostname`: "foo.com"
|
||||
// `host`: "foo.com:8888"
|
||||
let isChrome = isChromeScheme(location);
|
||||
|
||||
url.fileName = url.pathname ?
|
||||
(url.pathname.slice(url.pathname.lastIndexOf("/") + 1) || "/") :
|
||||
"/";
|
||||
|
||||
if (isChrome) {
|
||||
url.hostname = null;
|
||||
url.host = null;
|
||||
}
|
||||
|
||||
gURLStore.set(location, url);
|
||||
return url;
|
||||
} catch (e) {
|
||||
gURLStore.set(location, null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a source into a short and long name as well as a host name.
|
||||
*
|
||||
* @param {String} source
|
||||
* The source to parse. Can be a URI or names like "(eval)" or
|
||||
* "self-hosted".
|
||||
* @return {Object}
|
||||
* An object with the following properties:
|
||||
* - {String} short: A short name for the source.
|
||||
* - "http://page.com/test.js#go?q=query" -> "test.js"
|
||||
* - {String} long: The full, long name for the source, with
|
||||
hash/query stripped.
|
||||
* - "http://page.com/test.js#go?q=query" -> "http://page.com/test.js"
|
||||
* - {String?} host: If available, the host name for the source.
|
||||
* - "http://page.com/test.js#go?q=query" -> "page.com"
|
||||
*/
|
||||
function getSourceNames(source) {
|
||||
let data = gSourceNamesStore.get(source);
|
||||
|
||||
if (data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
let short, long, host;
|
||||
const sourceStr = source ? String(source) : "";
|
||||
|
||||
// If `data:...` uri
|
||||
if (isDataScheme(sourceStr)) {
|
||||
let commaIndex = sourceStr.indexOf(",");
|
||||
if (commaIndex > -1) {
|
||||
// The `short` name for a data URI becomes `data:` followed by the actual
|
||||
// encoded content, omitting the MIME type, and charset.
|
||||
short = `data:${sourceStr.substring(commaIndex + 1)}`.slice(0, 100);
|
||||
let result = { short, long: sourceStr };
|
||||
gSourceNamesStore.set(source, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const parsedUrl = parseURL(sourceStr);
|
||||
|
||||
if (!parsedUrl) {
|
||||
// Malformed URI.
|
||||
long = sourceStr;
|
||||
short = sourceStr.slice(0, 100);
|
||||
} else {
|
||||
host = parsedUrl.host;
|
||||
|
||||
long = parsedUrl.href;
|
||||
if (parsedUrl.hash) {
|
||||
long = long.replace(parsedUrl.hash, "");
|
||||
}
|
||||
if (parsedUrl.search) {
|
||||
long = long.replace(parsedUrl.search, "");
|
||||
}
|
||||
|
||||
short = parsedUrl.fileName;
|
||||
// If `short` is just a slash, and we actually have a path,
|
||||
// strip the slash and parse again to get a more useful short name.
|
||||
// e.g. "http://foo.com/bar/" -> "bar", rather than "/"
|
||||
if (short === "/" && parsedUrl.pathname !== "/") {
|
||||
short = parseURL(long.replace(/\/$/, "")).fileName;
|
||||
}
|
||||
}
|
||||
|
||||
if (!short) {
|
||||
if (!long) {
|
||||
long = UNKNOWN_SOURCE_STRING;
|
||||
}
|
||||
short = long.slice(0, 100);
|
||||
}
|
||||
|
||||
let result = { short, long, host };
|
||||
gSourceNamesStore.set(source, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// For the functions below, we assume that we will never access the location
|
||||
// argument out of bounds, which is indeed the vast majority of cases.
|
||||
//
|
||||
// They are written this way because they are hot. Each frame is checked for
|
||||
// being content or chrome when processing the profile.
|
||||
|
||||
function isColonSlashSlash(location, i = 0) {
|
||||
return location.charCodeAt(++i) === CHAR_CODE_COLON &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_SLASH &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_SLASH;
|
||||
}
|
||||
|
||||
function isDataScheme(location, i = 0) {
|
||||
return location.charCodeAt(i) === CHAR_CODE_D &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_A &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_T &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_A &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_COLON;
|
||||
}
|
||||
|
||||
function isContentScheme(location, i = 0) {
|
||||
let firstChar = location.charCodeAt(i);
|
||||
|
||||
switch (firstChar) {
|
||||
// "http://" or "https://"
|
||||
case CHAR_CODE_H:
|
||||
if (location.charCodeAt(++i) === CHAR_CODE_T &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_T &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_P) {
|
||||
if (location.charCodeAt(i + 1) === CHAR_CODE_S) {
|
||||
++i;
|
||||
}
|
||||
return isColonSlashSlash(location, i);
|
||||
}
|
||||
return false;
|
||||
|
||||
// "file://"
|
||||
case CHAR_CODE_F:
|
||||
if (location.charCodeAt(++i) === CHAR_CODE_I &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_L &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_E) {
|
||||
return isColonSlashSlash(location, i);
|
||||
}
|
||||
return false;
|
||||
|
||||
// "app://"
|
||||
case CHAR_CODE_A:
|
||||
if (location.charCodeAt(++i) == CHAR_CODE_P &&
|
||||
location.charCodeAt(++i) == CHAR_CODE_P) {
|
||||
return isColonSlashSlash(location, i);
|
||||
}
|
||||
return false;
|
||||
|
||||
// "blob:"
|
||||
case CHAR_CODE_B:
|
||||
if (
|
||||
location.charCodeAt(++i) == CHAR_CODE_L &&
|
||||
location.charCodeAt(++i) == CHAR_CODE_O &&
|
||||
location.charCodeAt(++i) == CHAR_CODE_B &&
|
||||
location.charCodeAt(++i) == CHAR_CODE_COLON
|
||||
) {
|
||||
return isContentScheme(location, i + 1);
|
||||
}
|
||||
return false;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isChromeScheme(location, i = 0) {
|
||||
let firstChar = location.charCodeAt(i);
|
||||
|
||||
switch (firstChar) {
|
||||
// "chrome://"
|
||||
case CHAR_CODE_C:
|
||||
if (location.charCodeAt(++i) === CHAR_CODE_H &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_R &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_O &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_M &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_E) {
|
||||
return isColonSlashSlash(location, i);
|
||||
}
|
||||
return false;
|
||||
|
||||
// "resource://"
|
||||
case CHAR_CODE_R:
|
||||
if (location.charCodeAt(++i) === CHAR_CODE_E &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_S &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_O &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_U &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_R &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_C &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_E) {
|
||||
return isColonSlashSlash(location, i);
|
||||
}
|
||||
return false;
|
||||
|
||||
// "jar:file://"
|
||||
case CHAR_CODE_J:
|
||||
if (location.charCodeAt(++i) === CHAR_CODE_A &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_R &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_COLON &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_F &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_I &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_L &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_E) {
|
||||
return isColonSlashSlash(location, i);
|
||||
}
|
||||
return false;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isWASM(location, i = 0) {
|
||||
return (
|
||||
// "wasm-function["
|
||||
location.charCodeAt(i) === CHAR_CODE_W &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_A &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_S &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_M &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_DASH &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_F &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_U &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_N &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_C &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_T &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_I &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_O &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_N &&
|
||||
location.charCodeAt(++i) === CHAR_CODE_L_SQUARE_BRACKET
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A utility method to get the file name from a sourcemapped location
|
||||
* The sourcemap location can be in any form. This method returns a
|
||||
* formatted file name for different cases like Windows or OSX.
|
||||
* @param source
|
||||
* @returns String
|
||||
*/
|
||||
function getSourceMappedFile(source) {
|
||||
// If sourcemapped source is a OSX path, return
|
||||
// the characters after last "/".
|
||||
// If sourcemapped source is a Windowss path, return
|
||||
// the characters after last "\\".
|
||||
if (source.lastIndexOf("/") >= 0) {
|
||||
source = source.slice(source.lastIndexOf("/") + 1);
|
||||
} else if (source.lastIndexOf("\\") >= 0) {
|
||||
source = source.slice(source.lastIndexOf("\\") + 1);
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseURL,
|
||||
getSourceNames,
|
||||
isChromeScheme,
|
||||
isContentScheme,
|
||||
isWASM,
|
||||
isDataScheme,
|
||||
getSourceMappedFile,
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
var localStorageMock = (() => {
|
||||
let store = {};
|
||||
return {
|
||||
getItem: (key) => {
|
||||
if (!store.hasOwnProperty(key)) {
|
||||
return "";
|
||||
}
|
||||
return store[key];
|
||||
},
|
||||
setItem: (key, value) => {
|
||||
store[key] = value.toString();
|
||||
},
|
||||
clear: () => {
|
||||
store = {};
|
||||
},
|
||||
removeItem: (key) => {
|
||||
delete store[key];
|
||||
},
|
||||
key: (index) => {
|
||||
return Object.keys(store)[index];
|
||||
},
|
||||
get length() {
|
||||
return Object.keys(store).length;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
Object.defineProperty(window, "localStorage", {
|
||||
value: localStorageMock
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
const { PrefsHelper } = require("../prefs");
|
||||
const Services = require("devtools-services");
|
||||
const pref = Services.pref;
|
||||
|
||||
describe("prefs helper", () => {
|
||||
beforeEach(() => {});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it("supports fallback values", () => {
|
||||
pref("devtools.valid", "{\"valid\": true}");
|
||||
pref("devtools.invalid", "{not valid at all]");
|
||||
pref("devtools.nodefault", "{not valid at all]");
|
||||
pref("devtools.validnodefault", "{\"nodefault\": true}");
|
||||
|
||||
const prefs = new PrefsHelper("devtools", {
|
||||
valid: ["Json", "valid", {valid: true}],
|
||||
invalid: ["Json", "invalid", {}],
|
||||
nodefault: ["Json", "nodefault"],
|
||||
validnodefault: ["Json", "validnodefault"],
|
||||
});
|
||||
|
||||
// Valid Json pref should return the actual value
|
||||
expect(prefs.valid).toEqual({valid: true});
|
||||
|
||||
// Invalid Json pref with a fallback shoud return the fallback value
|
||||
expect(prefs.invalid).toEqual({});
|
||||
|
||||
// Invalid Json pref with no fallback should throw
|
||||
expect(() => prefs.nodefault).toThrow();
|
||||
|
||||
// Valid Json pref with no fallback should return the value
|
||||
expect(prefs.validnodefault).toEqual({nodefault: true});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,200 @@
|
|||
const Services = require("devtools-services");
|
||||
const pref = Services.pref;
|
||||
|
||||
describe("services prefs shim", () => {
|
||||
beforeEach(() => {
|
||||
// Add some starter prefs.
|
||||
localStorage.setItem("Services.prefs:devtools.branch1.somebool", JSON.stringify({
|
||||
// bool
|
||||
type: 128,
|
||||
defaultValue: false,
|
||||
hasUserValue: false,
|
||||
userValue: false
|
||||
}));
|
||||
|
||||
localStorage.setItem("Services.prefs:devtools.branch1.somestring", JSON.stringify({
|
||||
// string
|
||||
type: 32,
|
||||
defaultValue: "dinosaurs",
|
||||
hasUserValue: true,
|
||||
userValue: "elephants"
|
||||
}));
|
||||
|
||||
localStorage.setItem("Services.prefs:devtools.branch2.someint", JSON.stringify({
|
||||
// int
|
||||
type: 64,
|
||||
defaultValue: -16,
|
||||
hasUserValue: false,
|
||||
userValue: null
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it("can get and set preferences", () => {
|
||||
expect(Services.prefs.getBoolPref("devtools.branch1.somebool")).toBe(false);
|
||||
Services.prefs.setBoolPref("devtools.branch1.somebool", true);
|
||||
expect(Services.prefs.getBoolPref("devtools.branch1.somebool")).toBe(true);
|
||||
|
||||
Services.prefs.clearUserPref("devtools.branch1.somestring");
|
||||
expect(Services.prefs.getCharPref("devtools.branch1.somestring")).toBe("dinosaurs");
|
||||
|
||||
expect(Services.prefs.prefHasUserValue("devtools.branch1.somebool")).toBe(true,
|
||||
"bool pref has user value");
|
||||
expect(Services.prefs.prefHasUserValue("devtools.branch1.somestring")).toBe(false,
|
||||
"string pref does not have user value");
|
||||
|
||||
// String prefs actually differ from Char prefs in the real implementation of
|
||||
// Services.prefs, but for this shim, both are using the same implementation.
|
||||
Services.prefs.setStringPref("devtools.branch1.somerealstring", "abcdef");
|
||||
expect(Services.prefs.getStringPref("devtools.branch1.somerealstring")).toBe("abcdef");
|
||||
});
|
||||
|
||||
it("can call savePrefFile without crashing", () => {
|
||||
Services.prefs.savePrefFile(null);
|
||||
});
|
||||
|
||||
it("can use branches", () => {
|
||||
let branch0 = Services.prefs.getBranch(null);
|
||||
let branch1 = Services.prefs.getBranch("devtools.branch1.");
|
||||
|
||||
branch1.setCharPref("somestring", "octopus");
|
||||
Services.prefs.setCharPref("devtools.branch1.somestring", "octopus");
|
||||
expect(Services.prefs.getCharPref("devtools.branch1.somestring")).toEqual("octopus");
|
||||
expect(branch0.getCharPref("devtools.branch1.somestring")).toEqual("octopus");
|
||||
expect(branch1.getCharPref("somestring")).toEqual("octopus");
|
||||
});
|
||||
|
||||
it("throws exceptions when expected", () => {
|
||||
expect(() => {
|
||||
Services.prefs.setIntPref("devtools.branch1.somebool", 27);
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
Services.prefs.setBoolPref("devtools.branch1.somebool", 27);
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
Services.prefs.getCharPref("devtools.branch2.someint");
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
Services.prefs.setCharPref("devtools.branch2.someint", "whatever");
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
Services.prefs.setIntPref("devtools.branch2.someint", "whatever");
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
Services.prefs.getBoolPref("devtools.branch1.somestring");
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
Services.prefs.setBoolPref("devtools.branch1.somestring", true);
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
Services.prefs.setCharPref("devtools.branch1.somestring", true);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("returns correct pref types", () => {
|
||||
expect(Services.prefs.getPrefType("devtools.branch1.somebool"))
|
||||
.toEqual(Services.prefs.PREF_BOOL);
|
||||
expect(Services.prefs.getPrefType("devtools.branch2.someint"))
|
||||
.toEqual(Services.prefs.PREF_INT);
|
||||
expect(Services.prefs.getPrefType("devtools.branch1.somestring"))
|
||||
.toEqual(Services.prefs.PREF_STRING);
|
||||
});
|
||||
|
||||
it("supports observers", () => {
|
||||
let notifications = {};
|
||||
let clearNotificationList = () => {
|
||||
notifications = {};
|
||||
};
|
||||
|
||||
let observer = {
|
||||
observe: function (subject, topic, data) {
|
||||
notifications[data] = true;
|
||||
}
|
||||
};
|
||||
|
||||
let branch0 = Services.prefs.getBranch(null);
|
||||
let branch1 = Services.prefs.getBranch("devtools.branch1.");
|
||||
|
||||
branch0.addObserver("devtools.branch1", null, null);
|
||||
branch0.addObserver("devtools.branch1.", observer);
|
||||
branch1.addObserver("", observer);
|
||||
|
||||
Services.prefs.setCharPref("devtools.branch1.somestring", "elf owl");
|
||||
expect(notifications).toEqual({
|
||||
"devtools.branch1.somestring": true,
|
||||
"somestring": true
|
||||
}, "notifications sent to two listeners");
|
||||
|
||||
clearNotificationList();
|
||||
Services.prefs.setIntPref("devtools.branch2.someint", 1729);
|
||||
expect(notifications).toEqual({}, "no notifications sent");
|
||||
|
||||
clearNotificationList();
|
||||
branch0.removeObserver("devtools.branch1.", observer);
|
||||
Services.prefs.setCharPref("devtools.branch1.somestring", "tapir");
|
||||
expect(notifications).toEqual({
|
||||
"somestring": true
|
||||
}, "removeObserver worked");
|
||||
|
||||
clearNotificationList();
|
||||
branch0.addObserver("devtools.branch1.somestring", observer);
|
||||
Services.prefs.setCharPref("devtools.branch1.somestring", "northern shoveler");
|
||||
expect(notifications).toEqual({
|
||||
"devtools.branch1.somestring": true,
|
||||
"somestring": true
|
||||
}, "notifications sent to two listeners");
|
||||
branch0.removeObserver("devtools.branch1.somestring", observer);
|
||||
|
||||
// Make sure we update if the pref change comes from somewhere else.
|
||||
clearNotificationList();
|
||||
pref("devtools.branch1.someotherstring", "lazuli bunting");
|
||||
expect(notifications).toEqual({
|
||||
"someotherstring": true
|
||||
}, "pref worked");
|
||||
});
|
||||
|
||||
it("does not crash when setting prefs (bug 1296427)", () => {
|
||||
// Regression test for bug 1296427.
|
||||
pref("devtools.hud.loglimit", 1000);
|
||||
pref("devtools.hud.loglimit.network", 1000);
|
||||
});
|
||||
|
||||
it("fixes observer bug (bug 1319150)", () => {
|
||||
// Regression test for bug 1319150.
|
||||
let seen = false;
|
||||
let fnObserver = () => {
|
||||
seen = true;
|
||||
};
|
||||
|
||||
let branch0 = Services.prefs.getBranch(null);
|
||||
branch0.addObserver("devtools.branch1.somestring", fnObserver);
|
||||
Services.prefs.setCharPref("devtools.branch1.somestring", "common merganser");
|
||||
expect(seen).toBe(true);
|
||||
branch0.removeObserver("devtools.branch1.somestring", fnObserver);
|
||||
});
|
||||
|
||||
it("supports default value argument", () => {
|
||||
// Check support for default values
|
||||
let intPrefWithDefault =
|
||||
Services.prefs.getIntPref("devtools.branch1.missing", 1);
|
||||
expect(intPrefWithDefault).toEqual(1);
|
||||
|
||||
let charPrefWithDefault =
|
||||
Services.prefs.getCharPref("devtools.branch1.missing", "test");
|
||||
expect(charPrefWithDefault).toEqual("test");
|
||||
|
||||
let boolPrefWithDefault =
|
||||
Services.prefs.getBoolPref("devtools.branch1.missing", true);
|
||||
expect(boolPrefWithDefault).toBe(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
const Services = require("devtools-services");
|
||||
|
||||
// const pref = Services;
|
||||
|
||||
describe("services telemetry shim", () => {
|
||||
beforeEach(() => {
|
||||
telemetry.scalars = {};
|
||||
telemetry.histograms = {};
|
||||
});
|
||||
|
||||
it("getHistogramById", () => {
|
||||
const hist = Services.telemetry.getHistogramById("foo");
|
||||
hist.add(3);
|
||||
expect(telemetry.histograms.foo).toEqual([3]);
|
||||
|
||||
hist.add(3);
|
||||
expect(telemetry.histograms.foo).toEqual([3, 3]);
|
||||
});
|
||||
|
||||
it("getKeyedHistogramById", () => {
|
||||
const hist = Services.telemetry.getKeyedHistogramById("foo");
|
||||
hist.add("a", 3);
|
||||
expect(telemetry.histograms.foo.a).toEqual([3]);
|
||||
|
||||
hist.add("a", 3);
|
||||
expect(telemetry.histograms.foo.a).toEqual([3, 3]);
|
||||
});
|
||||
|
||||
it("scalarSet", () => {
|
||||
Services.telemetry.scalarSet("foo", 3);
|
||||
expect(telemetry.scalars.foo).toEqual(3);
|
||||
});
|
||||
|
||||
it("scalarAdd", () => {
|
||||
Services.telemetry.scalarAdd("foo", 3);
|
||||
expect(telemetry.scalars.foo).toEqual(3);
|
||||
|
||||
Services.telemetry.scalarAdd("foo", 3);
|
||||
expect(telemetry.scalars.foo).toEqual(6);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,179 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const {
|
||||
getSourceMappedFile,
|
||||
getSourceNames,
|
||||
isChromeScheme,
|
||||
isContentScheme,
|
||||
isDataScheme,
|
||||
isWASM,
|
||||
parseURL,
|
||||
} = require("../source-utils");
|
||||
|
||||
describe("source-utils", () => {
|
||||
const CHROME_URLS = [
|
||||
"chrome://foo",
|
||||
"resource://baz",
|
||||
"jar:file:///Users/root"
|
||||
];
|
||||
const CONTENT_URLS = [
|
||||
"http://mozilla.org",
|
||||
"https://mozilla.org",
|
||||
"file:///Users/root",
|
||||
"app://fxosapp",
|
||||
"blob:http://mozilla.org",
|
||||
"blob:https://mozilla.org"
|
||||
];
|
||||
|
||||
it("parse URLs", () => {
|
||||
let parsed = parseURL("https://foo.com:8888/boo/bar.js?q=query");
|
||||
|
||||
expect(parsed.fileName).toBe("bar.js");
|
||||
expect(parsed.host).toBe("foo.com:8888");
|
||||
expect(parsed.hostname).toBe("foo.com");
|
||||
expect(parsed.port).toBe("8888");
|
||||
expect(parsed.href).toBe("https://foo.com:8888/boo/bar.js?q=query");
|
||||
|
||||
parsed = parseURL("https://foo.com");
|
||||
expect(parsed.host).toBe("foo.com");
|
||||
expect(parsed.hostname).toBe("foo.com");
|
||||
|
||||
expect(parseURL("self-hosted")).toBe(null);
|
||||
});
|
||||
|
||||
it("isContentScheme", () => {
|
||||
for (let url of CHROME_URLS) {
|
||||
expect(isContentScheme(url)).toBe(false);
|
||||
}
|
||||
for (let url of CONTENT_URLS) {
|
||||
expect(isContentScheme(url)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("isChromeScheme", () => {
|
||||
for (let url of CHROME_URLS) {
|
||||
expect(isChromeScheme(url)).toBe(true);
|
||||
}
|
||||
for (let url of CONTENT_URLS) {
|
||||
expect(isChromeScheme(url)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("isWASM", () => {
|
||||
expect(isWASM("wasm-function[66240] (?:13870536)")).toBe(true);
|
||||
expect(isWASM(CHROME_URLS[0])).toBe(false);
|
||||
});
|
||||
|
||||
it("isDataScheme", () => {
|
||||
const dataURI = "data:text/html;charset=utf-8,<!DOCTYPE html></html>";
|
||||
expect(isDataScheme(dataURI)).toBe(true);
|
||||
|
||||
for (let url of CHROME_URLS) {
|
||||
expect(isDataScheme(url)).toBe(false);
|
||||
}
|
||||
for (let url of CONTENT_URLS) {
|
||||
expect(isDataScheme(url)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("getSourceMappedFile", () => {
|
||||
expect(getSourceMappedFile("baz.js")).toBe("baz.js");
|
||||
expect(getSourceMappedFile("/foo/bar/baz.js")).toBe("baz.js");
|
||||
expect(getSourceMappedFile("Z:\\foo\\bar\\baz.js")).toBe("baz.js");
|
||||
});
|
||||
|
||||
it("getSourceNames", () => {
|
||||
// Check length
|
||||
const longMalformedURL = `example.com${"/a".repeat(100)}/file.js`;
|
||||
expect(getSourceNames(longMalformedURL).short.length).toBeLessThanOrEqual(100);
|
||||
|
||||
expect(getSourceNames("http://example.com/foo/bar/baz/boo.js")).toEqual({
|
||||
short: "boo.js",
|
||||
long: "http://example.com/foo/bar/baz/boo.js",
|
||||
host: "example.com"
|
||||
});
|
||||
|
||||
expect(getSourceNames("self-hosted")).toEqual({
|
||||
short: "self-hosted",
|
||||
long: "self-hosted",
|
||||
host: undefined
|
||||
});
|
||||
|
||||
expect(getSourceNames("")).toEqual({
|
||||
short: "(unknown)",
|
||||
long: "(unknown)",
|
||||
host: undefined
|
||||
});
|
||||
|
||||
// Test shortening data URIs, stripping mime/charset
|
||||
const dataURI = "data:text/html;charset=utf-8,<!DOCTYPE html></html>";
|
||||
expect(getSourceNames(dataURI)).toEqual({
|
||||
short: "data:<!DOCTYPE html></html>",
|
||||
long: "data:text/html;charset=utf-8,<!DOCTYPE html></html>",
|
||||
host: undefined
|
||||
});
|
||||
|
||||
// Test shortening data URIs and that the `short` result is capped
|
||||
let longDataURI = `data:image/png;base64,${"a".repeat(100)}`;
|
||||
let longDataURIShort = getSourceNames(longDataURI).short;
|
||||
expect(longDataURIShort.length).toBeLessThanOrEqual(100);
|
||||
expect(longDataURIShort.substr(0, 10)).toBe("data:aaaaa");
|
||||
|
||||
// Test simple URL and cache retrieval by calling the same input multiple times.
|
||||
let testUrl = "http://example.com/foo/bar/baz/boo.js";
|
||||
expect(getSourceNames(testUrl)).toEqual({
|
||||
short: "boo.js",
|
||||
long: testUrl,
|
||||
host: "example.com"
|
||||
});
|
||||
expect(getSourceNames(testUrl)).toEqual({
|
||||
short: "boo.js",
|
||||
long: testUrl,
|
||||
host: "example.com"
|
||||
});
|
||||
|
||||
// Check query and hash and port
|
||||
expect(getSourceNames("http://example.com:8888/foo/bar/baz.js?q=query#go")).toEqual({
|
||||
short: "baz.js",
|
||||
long: "http://example.com:8888/foo/bar/baz.js",
|
||||
host: "example.com:8888"
|
||||
});
|
||||
|
||||
// Trailing "/" with nothing beyond host
|
||||
expect(getSourceNames("http://example.com/")).toEqual({
|
||||
short: "/",
|
||||
long: "http://example.com/",
|
||||
host: "example.com"
|
||||
});
|
||||
|
||||
// Trailing "/"
|
||||
expect(getSourceNames("http://example.com/foo/bar/")).toEqual({
|
||||
short: "bar",
|
||||
long: "http://example.com/foo/bar/",
|
||||
host: "example.com"
|
||||
});
|
||||
|
||||
// Non-extension ending
|
||||
expect(getSourceNames("http://example.com/bar")).toEqual({
|
||||
short: "bar",
|
||||
long: "http://example.com/bar",
|
||||
host: "example.com"
|
||||
});
|
||||
|
||||
// Check query
|
||||
expect(getSourceNames("http://example.com/foo.js?bar=1&baz=2")).toEqual({
|
||||
short: "foo.js",
|
||||
long: "http://example.com/foo.js",
|
||||
host: "example.com"
|
||||
});
|
||||
|
||||
// Check query with trailing slash
|
||||
expect(getSourceNames("http://example.com/foo/?bar=1&baz=2")).toEqual({
|
||||
short: "foo",
|
||||
long: "http://example.com/foo/",
|
||||
host: "example.com"
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,120 @@
|
|||
const Telemetry = require("../utils/telemetry");
|
||||
const telemetry = new Telemetry();
|
||||
|
||||
describe("telemetry shim", () => {
|
||||
it("msSystemNow", () => {
|
||||
expect(() => {
|
||||
telemetry.msSystemNow;
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("start", () => {
|
||||
expect(() => {
|
||||
telemetry.start("foo", this);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("startKeyed", () => {
|
||||
expect(() => {
|
||||
telemetry.startKeyed("foo", "bar", this);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("finish", () => {
|
||||
expect(() => {
|
||||
telemetry.finish("foo", this);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("finishKeyed", () => {
|
||||
expect(() => {
|
||||
telemetry.finishKeyed("foo", "bar", this);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("getHistogramById", () => {
|
||||
expect(() => {
|
||||
telemetry.getHistogramById("foo").add(3);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("getKeyedHistogramById", () => {
|
||||
expect(() => {
|
||||
telemetry.getKeyedHistogramById("foo").add("foo", 3);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("scalarSet", () => {
|
||||
expect(() => {
|
||||
telemetry.scalarSet("foo", 3);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("scalarAdd", () => {
|
||||
expect(() => {
|
||||
telemetry.scalarAdd("foo", 3);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("keyedScalarAdd", () => {
|
||||
expect(() => {
|
||||
telemetry.keyedScalarAdd("foo", "bar", 3);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("setEventRecordingEnabled", () => {
|
||||
expect(() => {
|
||||
telemetry.setEventRecordingEnabled("foo", true);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("preparePendingEvent", () => {
|
||||
expect(() => {
|
||||
telemetry.preparePendingEvent(
|
||||
"devtools.main", "open", "inspector", null, ["foo", "bar"]);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("addEventProperty", () => {
|
||||
expect(() => {
|
||||
telemetry.addEventProperty(
|
||||
"devtools.main", "open", "inspector", "foo", "1");
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("addEventProperties", () => {
|
||||
expect(() => {
|
||||
telemetry.addEventProperties("devtools.main", "open", "inspector", {
|
||||
"foo": "1",
|
||||
"bar": "2"
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("_sendPendingEvent", () => {
|
||||
expect(() => {
|
||||
telemetry._sendPendingEvent("devtools.main", "open", "inspector", null);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("recordEvent", () => {
|
||||
expect(() => {
|
||||
telemetry.recordEvent("devtools.main", "open", "inspector", null, {
|
||||
"foo": "1",
|
||||
"bar": "2"
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("toolOpened", () => {
|
||||
expect(() => {
|
||||
telemetry.toolOpened("foo");
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("toolClosed", () => {
|
||||
expect(() => {
|
||||
telemetry.toolClosed("foo");
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,228 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { getUnicodeUrl, getUnicodeUrlPath, getUnicodeHostname } =
|
||||
require("../unicode-url");
|
||||
|
||||
describe("unicode-url", () => {
|
||||
// List of URLs used to test Unicode URL conversion
|
||||
const TEST_URLS = [
|
||||
// Type: Readable ASCII URLs
|
||||
// Expected: All of Unicode versions should equal to the raw.
|
||||
{
|
||||
raw: "https://example.org",
|
||||
expectedUnicode: "https://example.org",
|
||||
},
|
||||
{
|
||||
raw: "http://example.org",
|
||||
expectedUnicode: "http://example.org",
|
||||
},
|
||||
{
|
||||
raw: "ftp://example.org",
|
||||
expectedUnicode: "ftp://example.org",
|
||||
},
|
||||
{
|
||||
raw: "https://example.org.",
|
||||
expectedUnicode: "https://example.org.",
|
||||
},
|
||||
{
|
||||
raw: "https://example.org/",
|
||||
expectedUnicode: "https://example.org/",
|
||||
},
|
||||
{
|
||||
raw: "https://example.org/test",
|
||||
expectedUnicode: "https://example.org/test",
|
||||
},
|
||||
{
|
||||
raw: "https://example.org/test.html",
|
||||
expectedUnicode: "https://example.org/test.html",
|
||||
},
|
||||
{
|
||||
raw: "https://example.org/test.html?one=1&two=2",
|
||||
expectedUnicode: "https://example.org/test.html?one=1&two=2",
|
||||
},
|
||||
{
|
||||
raw: "https://example.org/test.html#here",
|
||||
expectedUnicode: "https://example.org/test.html#here",
|
||||
},
|
||||
{
|
||||
raw: "https://example.org/test.html?one=1&two=2#here",
|
||||
expectedUnicode: "https://example.org/test.html?one=1&two=2#here",
|
||||
},
|
||||
// Type: Unreadable URLs with either Punycode domain names or URI-encoded
|
||||
// paths
|
||||
// Expected: Unreadable domain names and URI-encoded paths should be converted
|
||||
// to readable Unicode.
|
||||
{
|
||||
raw: "https://xn--g6w.xn--8pv/test.html",
|
||||
// Do not type Unicode characters directly, because this test file isn't
|
||||
// specified with a known encoding.
|
||||
expectedUnicode: "https://\u6e2c.\u672c/test.html",
|
||||
},
|
||||
{
|
||||
raw: "https://example.org/%E6%B8%AC%E8%A9%A6.html",
|
||||
// Do not type Unicode characters directly, because this test file isn't
|
||||
// specified with a known encoding.
|
||||
expectedUnicode: "https://example.org/\u6e2c\u8a66.html",
|
||||
},
|
||||
{
|
||||
raw: "https://example.org/test.html?One=%E4%B8%80",
|
||||
// Do not type Unicode characters directly, because this test file isn't
|
||||
// specified with a known encoding.
|
||||
expectedUnicode: "https://example.org/test.html?One=\u4e00",
|
||||
},
|
||||
{
|
||||
raw: "https://example.org/test.html?%E4%B8%80=1",
|
||||
// Do not type Unicode characters directly, because this test file isn't
|
||||
// specified with a known encoding.
|
||||
expectedUnicode: "https://example.org/test.html?\u4e00=1",
|
||||
},
|
||||
{
|
||||
raw: "https://xn--g6w.xn--8pv/%E6%B8%AC%E8%A9%A6.html" +
|
||||
"?%E4%B8%80=%E4%B8%80" +
|
||||
"#%E6%AD%A4",
|
||||
// Do not type Unicode characters directly, because this test file isn't
|
||||
// specified with a known encoding.
|
||||
expectedUnicode: "https://\u6e2c.\u672c/\u6e2c\u8a66.html" +
|
||||
"?\u4e00=\u4e00" +
|
||||
"#\u6b64",
|
||||
},
|
||||
// Type: data: URIs
|
||||
// Expected: All should not be converted.
|
||||
{
|
||||
raw: "data:text/plain;charset=UTF-8;Hello%20world",
|
||||
expectedUnicode: "data:text/plain;charset=UTF-8;Hello%20world",
|
||||
},
|
||||
{
|
||||
raw: "data:text/plain;charset=UTF-8;%E6%B8%AC%20%E8%A9%A6",
|
||||
expectedUnicode: "data:text/plain;charset=UTF-8;%E6%B8%AC%20%E8%A9%A6",
|
||||
},
|
||||
{
|
||||
raw: "data:image/png;base64,iVBORw0KGgoAAA" +
|
||||
"ANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4" +
|
||||
"//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU" +
|
||||
"5ErkJggg==",
|
||||
expectedUnicode: "data:image/png;base64,iVBORw0KGgoAAA" +
|
||||
"ANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4" +
|
||||
"//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU" +
|
||||
"5ErkJggg==",
|
||||
},
|
||||
// Type: Malformed URLs
|
||||
// Expected: All should not be converted.
|
||||
{
|
||||
raw: "://example.org/test",
|
||||
expectedUnicode: "://example.org/test",
|
||||
},
|
||||
{
|
||||
raw: "://xn--g6w.xn--8pv/%E6%B8%AC%E8%A9%A6.html" +
|
||||
"?%E4%B8%80=%E4%B8%80",
|
||||
expectedUnicode: "://xn--g6w.xn--8pv/%E6%B8%AC%E8%A9%A6.html" +
|
||||
"?%E4%B8%80=%E4%B8%80",
|
||||
},
|
||||
{
|
||||
// %E8%A9 isn't a valid UTF-8 code, so this URL is malformed.
|
||||
raw: "https://xn--g6w.xn--8pv/%E6%B8%AC%E8%A9",
|
||||
expectedUnicode: "https://xn--g6w.xn--8pv/%E6%B8%AC%E8%A9",
|
||||
},
|
||||
];
|
||||
|
||||
// List of hostanmes used to test Unicode hostname conversion
|
||||
const TEST_HOSTNAMES = [
|
||||
// Type: Readable ASCII hostnames
|
||||
// Expected: All of Unicode versions should equal to the raw.
|
||||
{
|
||||
raw: "example",
|
||||
expectedUnicode: "example",
|
||||
},
|
||||
{
|
||||
raw: "example.org",
|
||||
expectedUnicode: "example.org",
|
||||
},
|
||||
// Type: Unreadable Punycode hostnames
|
||||
// Expected: Punycode should be converted to readable Unicode.
|
||||
{
|
||||
raw: "xn--g6w",
|
||||
// Do not type Unicode characters directly, because this test file isn't
|
||||
// specified with a known encoding.
|
||||
expectedUnicode: "\u6e2c",
|
||||
},
|
||||
{
|
||||
raw: "xn--g6w.xn--8pv",
|
||||
// Do not type Unicode characters directly, because this test file isn't
|
||||
// specified with a known encoding.
|
||||
expectedUnicode: "\u6e2c.\u672c",
|
||||
},
|
||||
];
|
||||
|
||||
// List of URL paths used to test Unicode URL path conversion
|
||||
const TEST_URL_PATHS = [
|
||||
// Type: Readable ASCII URL paths
|
||||
// Expected: All of Unicode versions should equal to the raw.
|
||||
{
|
||||
raw: "test",
|
||||
expectedUnicode: "test",
|
||||
},
|
||||
{
|
||||
raw: "/",
|
||||
expectedUnicode: "/",
|
||||
},
|
||||
{
|
||||
raw: "/test",
|
||||
expectedUnicode: "/test",
|
||||
},
|
||||
{
|
||||
raw: "/test.html?one=1&two=2#here",
|
||||
expectedUnicode: "/test.html?one=1&two=2#here",
|
||||
},
|
||||
// Type: Unreadable URI-encoded URL paths
|
||||
// Expected: URL paths should be converted to readable Unicode.
|
||||
{
|
||||
raw: "/%E6%B8%AC%E8%A9%A6",
|
||||
// Do not type Unicode characters directly, because this test file isn't
|
||||
// specified with a known encoding.
|
||||
expectedUnicode: "/\u6e2c\u8a66",
|
||||
},
|
||||
{
|
||||
raw: "/%E6%B8%AC%E8%A9%A6.html",
|
||||
// Do not type Unicode characters directly, because this test file isn't
|
||||
// specified with a known encoding.
|
||||
expectedUnicode: "/\u6e2c\u8a66.html",
|
||||
},
|
||||
{
|
||||
raw: "/%E6%B8%AC%E8%A9%A6.html" +
|
||||
"?%E4%B8%80=%E4%B8%80&%E4%BA%8C=%E4%BA%8C" +
|
||||
"#%E6%AD%A4",
|
||||
// Do not type Unicode characters directly, because this test file isn't
|
||||
// specified with a known encoding.
|
||||
expectedUnicode: "/\u6e2c\u8a66.html" +
|
||||
"?\u4e00=\u4e00&\u4e8c=\u4e8c" +
|
||||
"#\u6b64",
|
||||
},
|
||||
// Type: Malformed URL paths
|
||||
// Expected: All should not be converted.
|
||||
{
|
||||
// %E8%A9 isn't a valid UTF-8 code, so this URL is malformed.
|
||||
raw: "/%E6%B8%AC%E8%A9",
|
||||
expectedUnicode: "/%E6%B8%AC%E8%A9",
|
||||
},
|
||||
];
|
||||
|
||||
it("Get Unicode URLs", () => {
|
||||
for (let url of TEST_URLS) {
|
||||
expect(getUnicodeUrl(url.raw)).toBe(url.expectedUnicode);
|
||||
}
|
||||
});
|
||||
|
||||
it("Get Unicode hostnames", () => {
|
||||
for (let hostname of TEST_HOSTNAMES) {
|
||||
expect(getUnicodeHostname(hostname.raw)).toBe(hostname.expectedUnicode);
|
||||
}
|
||||
});
|
||||
|
||||
it("Get Unicode URL paths", () => {
|
||||
for (let urlPath of TEST_URL_PATHS) {
|
||||
expect(getUnicodeUrlPath(urlPath.raw)).toBe(urlPath.expectedUnicode);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,115 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// This file is a chrome-API-free version of the module
|
||||
// devtools/client/shared/unicode-url.js in the mozilla-central repository, so
|
||||
// that it can be used in Chrome-API-free applications, such as the Launchpad.
|
||||
// But because of this, it cannot take advantage of utilizing chrome APIs and
|
||||
// should implement the similar functionalities on its own.
|
||||
//
|
||||
// Please keep in mind that if the feature in this file has changed, don't
|
||||
// forget to also change that accordingly in
|
||||
// devtools/client/shared/unicode-url.js in the mozilla-central repository.
|
||||
|
||||
"use strict";
|
||||
|
||||
const punycode = require("punycode");
|
||||
|
||||
/**
|
||||
* Gets a readble Unicode hostname from a hostname.
|
||||
*
|
||||
* If the `hostname` is a readable ASCII hostname, such as example.org, then
|
||||
* this function will simply return the original `hostname`.
|
||||
*
|
||||
* If the `hostname` is a Punycode hostname representing a Unicode domain name,
|
||||
* such as xn--g6w.xn--8pv, then this function will return the readable Unicode
|
||||
* domain name by decoding the Punycode hostname.
|
||||
*
|
||||
* @param {string} hostname
|
||||
* the hostname from which the Unicode hostname will be
|
||||
* parsed, such as example.org, xn--g6w.xn--8pv.
|
||||
* @return {string} The Unicode hostname. It may be the same as the `hostname`
|
||||
* passed to this function if the `hostname` itself is
|
||||
* a readable ASCII hostname or a Unicode hostname.
|
||||
*/
|
||||
function getUnicodeHostname(hostname) {
|
||||
try {
|
||||
return punycode.toUnicode(hostname);
|
||||
} catch (err) {
|
||||
}
|
||||
return hostname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a readble Unicode URL pathname from a URL pathname.
|
||||
*
|
||||
* If the `urlPath` is a readable ASCII URL pathname, such as /a/b/c.js, then
|
||||
* this function will simply return the original `urlPath`.
|
||||
*
|
||||
* If the `urlPath` is a URI-encoded pathname, such as %E8%A9%A6/%E6%B8%AC.js,
|
||||
* then this function will return the readable Unicode pathname.
|
||||
*
|
||||
* If the `urlPath` is a malformed URL pathname, then this function will simply
|
||||
* return the original `urlPath`.
|
||||
*
|
||||
* @param {string} urlPath
|
||||
* the URL path from which the Unicode URL path will be parsed,
|
||||
* such as /a/b/c.js, %E8%A9%A6/%E6%B8%AC.js.
|
||||
* @return {string} The Unicode URL Path. It may be the same as the `urlPath`
|
||||
* passed to this function if the `urlPath` itself is a readable
|
||||
* ASCII url or a Unicode url.
|
||||
*/
|
||||
function getUnicodeUrlPath(urlPath) {
|
||||
try {
|
||||
return decodeURIComponent(urlPath);
|
||||
} catch (err) {
|
||||
}
|
||||
return urlPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a readable Unicode URL from a URL.
|
||||
*
|
||||
* If the `url` is a readable ASCII URL, such as http://example.org/a/b/c.js,
|
||||
* then this function will simply return the original `url`.
|
||||
*
|
||||
* If the `url` includes either an unreadable Punycode domain name or an
|
||||
* unreadable URI-encoded pathname, such as
|
||||
* http://xn--g6w.xn--8pv/%E8%A9%A6/%E6%B8%AC.js, then this function will return
|
||||
* the readable URL by decoding all its unreadable URL components to Unicode
|
||||
* characters.
|
||||
*
|
||||
* If the `url` is a malformed URL, then this function will return the original
|
||||
* `url`.
|
||||
*
|
||||
* If the `url` is a data: URI, then this function will return the original
|
||||
* `url`.
|
||||
*
|
||||
* @param {string} url
|
||||
* the full URL, or a data: URI. from which the readable URL
|
||||
* will be parsed, such as, http://example.org/a/b/c.js,
|
||||
* http://xn--g6w.xn--8pv/%E8%A9%A6/%E6%B8%AC.js
|
||||
* @return {string} The readable URL. It may be the same as the `url` passed to
|
||||
* this function if the `url` itself is readable.
|
||||
*/
|
||||
function getUnicodeUrl(url) {
|
||||
try {
|
||||
const { protocol, hostname } = new URL(url);
|
||||
if (protocol === "data:") {
|
||||
// Never convert a data: URI.
|
||||
return url;
|
||||
}
|
||||
const readableHostname = getUnicodeHostname(hostname);
|
||||
url = decodeURIComponent(url);
|
||||
return url.replace(hostname, readableHostname);
|
||||
} catch (err) {
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getUnicodeHostname,
|
||||
getUnicodeUrlPath,
|
||||
getUnicodeUrl,
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
// See bug 1273941 to understand this choice of promise.
|
||||
const Promise = require("../sham/promise");
|
||||
|
||||
/**
|
||||
* Returns a deferred object, with a resolve and reject property.
|
||||
* https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm/Deferred
|
||||
*/
|
||||
module.exports = function defer() {
|
||||
let resolve, reject;
|
||||
let promise = new Promise(function () {
|
||||
resolve = arguments[0];
|
||||
reject = arguments[1];
|
||||
});
|
||||
return {
|
||||
resolve: resolve,
|
||||
reject: reject,
|
||||
promise: promise
|
||||
};
|
||||
};
|
|
@ -0,0 +1,127 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
var EventEmitter = function EventEmitter() {};
|
||||
module.exports = EventEmitter;
|
||||
|
||||
const promise = require("./promise");
|
||||
|
||||
/**
|
||||
* Decorate an object with event emitter functionality.
|
||||
*
|
||||
* @param Object aObjectToDecorate
|
||||
* Bind all public methods of EventEmitter to
|
||||
* the aObjectToDecorate object.
|
||||
*/
|
||||
EventEmitter.decorate = function EventEmitter_decorate (aObjectToDecorate) {
|
||||
let emitter = new EventEmitter();
|
||||
aObjectToDecorate.on = emitter.on.bind(emitter);
|
||||
aObjectToDecorate.off = emitter.off.bind(emitter);
|
||||
aObjectToDecorate.once = emitter.once.bind(emitter);
|
||||
aObjectToDecorate.emit = emitter.emit.bind(emitter);
|
||||
};
|
||||
|
||||
EventEmitter.prototype = {
|
||||
/**
|
||||
* Connect a listener.
|
||||
*
|
||||
* @param string aEvent
|
||||
* The event name to which we're connecting.
|
||||
* @param function aListener
|
||||
* Called when the event is fired.
|
||||
*/
|
||||
on: function EventEmitter_on(aEvent, aListener) {
|
||||
if (!this._eventEmitterListeners)
|
||||
this._eventEmitterListeners = new Map();
|
||||
if (!this._eventEmitterListeners.has(aEvent)) {
|
||||
this._eventEmitterListeners.set(aEvent, []);
|
||||
}
|
||||
this._eventEmitterListeners.get(aEvent).push(aListener);
|
||||
},
|
||||
|
||||
/**
|
||||
* Listen for the next time an event is fired.
|
||||
*
|
||||
* @param string aEvent
|
||||
* The event name to which we're connecting.
|
||||
* @param function aListener
|
||||
* (Optional) Called when the event is fired. Will be called at most
|
||||
* one time.
|
||||
* @return promise
|
||||
* A promise which is resolved when the event next happens. The
|
||||
* resolution value of the promise is the first event argument. If
|
||||
* you need access to second or subsequent event arguments (it's rare
|
||||
* that this is needed) then use aListener
|
||||
*/
|
||||
once: function EventEmitter_once(aEvent, aListener) {
|
||||
let deferred = promise.defer();
|
||||
|
||||
let handler = (aEvent, aFirstArg, ...aRest) => {
|
||||
this.off(aEvent, handler);
|
||||
if (aListener) {
|
||||
aListener.apply(null, [aEvent, aFirstArg, ...aRest]);
|
||||
}
|
||||
deferred.resolve(aFirstArg);
|
||||
};
|
||||
|
||||
handler._originalListener = aListener;
|
||||
this.on(aEvent, handler);
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a previously-registered event listener. Works for events
|
||||
* registered with either on or once.
|
||||
*
|
||||
* @param string aEvent
|
||||
* The event name whose listener we're disconnecting.
|
||||
* @param function aListener
|
||||
* The listener to remove.
|
||||
*/
|
||||
off: function EventEmitter_off(aEvent, aListener) {
|
||||
if (!this._eventEmitterListeners)
|
||||
return;
|
||||
let listeners = this._eventEmitterListeners.get(aEvent);
|
||||
if (listeners) {
|
||||
this._eventEmitterListeners.set(aEvent, listeners.filter(l => {
|
||||
return l !== aListener && l._originalListener !== aListener;
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Emit an event. All arguments to this method will
|
||||
* be sent to listener functions.
|
||||
*/
|
||||
emit: function EventEmitter_emit(aEvent) {
|
||||
if (!this._eventEmitterListeners || !this._eventEmitterListeners.has(aEvent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let originalListeners = this._eventEmitterListeners.get(aEvent);
|
||||
for (let listener of this._eventEmitterListeners.get(aEvent)) {
|
||||
// If the object was destroyed during event emission, stop
|
||||
// emitting.
|
||||
if (!this._eventEmitterListeners) {
|
||||
break;
|
||||
}
|
||||
|
||||
// If listeners were removed during emission, make sure the
|
||||
// event handler we're going to fire wasn't removed.
|
||||
if (originalListeners === this._eventEmitterListeners.get(aEvent) ||
|
||||
this._eventEmitterListeners.get(aEvent).some(l => l === listener)) {
|
||||
try {
|
||||
listener.apply(null, arguments);
|
||||
}
|
||||
catch (ex) {
|
||||
// Prevent a bad listener from interfering with the others.
|
||||
let msg = ex + ": " + ex.stack;
|
||||
//console.error(msg);
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/*
|
||||
* A sham for https://dxr.mozilla.org/mozilla-central/source/toolkit/modules/Promise.jsm
|
||||
*/
|
||||
|
||||
/**
|
||||
* Promise.jsm is mostly the Promise web API with a `defer` method. Just drop this in here,
|
||||
* and use the native web API (although building with webpack/babel, it may replace this
|
||||
* with it's own version if we want to target environments that do not have `Promise`.
|
||||
*/
|
||||
|
||||
let p = typeof window != "undefined" ? window.Promise : Promise;
|
||||
p.defer = function defer() {
|
||||
var resolve, reject;
|
||||
var promise = new Promise(function() {
|
||||
resolve = arguments[0];
|
||||
reject = arguments[1];
|
||||
});
|
||||
return {
|
||||
resolve: resolve,
|
||||
reject: reject,
|
||||
promise: promise,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = p;
|
|
@ -0,0 +1,515 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
/* eslint-disable spaced-comment */
|
||||
/* globals StopIteration */
|
||||
|
||||
/**
|
||||
* This module implements a subset of "Task.js" <http://taskjs.org/>.
|
||||
* It is a copy of toolkit/modules/Task.jsm. Please try not to
|
||||
* diverge the API here.
|
||||
*
|
||||
* Paraphrasing from the Task.js site, tasks make sequential, asynchronous
|
||||
* operations simple, using the power of JavaScript's "yield" operator.
|
||||
*
|
||||
* Tasks are built upon generator functions and promises, documented here:
|
||||
*
|
||||
* <https://developer.mozilla.org/en/JavaScript/Guide/Iterators_and_Generators>
|
||||
* <http://wiki.commonjs.org/wiki/Promises/A>
|
||||
*
|
||||
* The "Task.spawn" function takes a generator function and starts running it as
|
||||
* a task. Every time the task yields a promise, it waits until the promise is
|
||||
* fulfilled. "Task.spawn" returns a promise that is resolved when the task
|
||||
* completes successfully, or is rejected if an exception occurs.
|
||||
*
|
||||
* -----------------------------------------------------------------------------
|
||||
*
|
||||
* const {Task} = require("devtools/shared/task");
|
||||
*
|
||||
* Task.spawn(function* () {
|
||||
*
|
||||
* // This is our task. Let's create a promise object, wait on it and capture
|
||||
* // its resolution value.
|
||||
* let myPromise = getPromiseResolvedOnTimeoutWithValue(1000, "Value");
|
||||
* let result = yield myPromise;
|
||||
*
|
||||
* // This part is executed only after the promise above is fulfilled (after
|
||||
* // one second, in this imaginary example). We can easily loop while
|
||||
* // calling asynchronous functions, and wait multiple times.
|
||||
* for (let i = 0; i < 3; i++) {
|
||||
* result += yield getPromiseResolvedOnTimeoutWithValue(50, "!");
|
||||
* }
|
||||
*
|
||||
* return "Resolution result for the task: " + result;
|
||||
* }).then(function (result) {
|
||||
*
|
||||
* // result == "Resolution result for the task: Value!!!"
|
||||
*
|
||||
* // The result is undefined if no value was returned.
|
||||
*
|
||||
* }, function (exception) {
|
||||
*
|
||||
* // Failure! We can inspect or report the exception.
|
||||
*
|
||||
* });
|
||||
*
|
||||
* -----------------------------------------------------------------------------
|
||||
*
|
||||
* This module implements only the "Task.js" interfaces described above, with no
|
||||
* additional features to control the task externally, or do custom scheduling.
|
||||
* It also provides the following extensions that simplify task usage in the
|
||||
* most common cases:
|
||||
*
|
||||
* - The "Task.spawn" function also accepts an iterator returned by a generator
|
||||
* function, in addition to a generator function. This way, you can call into
|
||||
* the generator function with the parameters you want, and with "this" bound
|
||||
* to the correct value. Also, "this" is never bound to the task object when
|
||||
* "Task.spawn" calls the generator function.
|
||||
*
|
||||
* - In addition to a promise object, a task can yield the iterator returned by
|
||||
* a generator function. The iterator is turned into a task automatically.
|
||||
* This reduces the syntax overhead of calling "Task.spawn" explicitly when
|
||||
* you want to recurse into other task functions.
|
||||
*
|
||||
* - The "Task.spawn" function also accepts a primitive value, or a function
|
||||
* returning a primitive value, and treats the value as the result of the
|
||||
* task. This makes it possible to call an externally provided function and
|
||||
* spawn a task from it, regardless of whether it is an asynchronous generator
|
||||
* or a synchronous function. This comes in handy when iterating over
|
||||
* function lists where some items have been converted to tasks and some not.
|
||||
*/
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Globals
|
||||
|
||||
const Promise = require("../sham/promise");
|
||||
const defer = require("./defer");
|
||||
|
||||
// The following error types are considered programmer errors, which should be
|
||||
// reported (possibly redundantly) so as to let programmers fix their code.
|
||||
const ERRORS_TO_REPORT = ["EvalError", "RangeError", "ReferenceError",
|
||||
"TypeError"];
|
||||
|
||||
/**
|
||||
* The Task currently being executed
|
||||
*/
|
||||
var gCurrentTask = null;
|
||||
|
||||
/**
|
||||
* If `true`, capture stacks whenever entering a Task and rewrite the
|
||||
* stack any exception thrown through a Task.
|
||||
*/
|
||||
var gMaintainStack = false;
|
||||
|
||||
/**
|
||||
* Iterate through the lines of a string.
|
||||
*
|
||||
* @return Iterator<string>
|
||||
*/
|
||||
function* linesOf(string) {
|
||||
let reLine = /([^\r\n])+/g;
|
||||
let match;
|
||||
while ((match = reLine.exec(string))) {
|
||||
yield [match[0], match.index];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect whether a value is a generator.
|
||||
*
|
||||
* @param aValue
|
||||
* The value to identify.
|
||||
* @return A boolean indicating whether the value is a generator.
|
||||
*/
|
||||
function isGenerator(value) {
|
||||
return Object.prototype.toString.call(value) == "[object Generator]";
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Task
|
||||
|
||||
/**
|
||||
* This object provides the public module functions.
|
||||
*/
|
||||
var Task = {
|
||||
/**
|
||||
* Creates and starts a new task.
|
||||
*
|
||||
* @param task
|
||||
* - If you specify a generator function, it is called with no
|
||||
* arguments to retrieve the associated iterator. The generator
|
||||
* function is a task, that is can yield promise objects to wait
|
||||
* upon.
|
||||
* - If you specify the iterator returned by a generator function you
|
||||
* called, the generator function is also executed as a task. This
|
||||
* allows you to call the function with arguments.
|
||||
* - If you specify a function that is not a generator, it is called
|
||||
* with no arguments, and its return value is used to resolve the
|
||||
* returned promise.
|
||||
* - If you specify anything else, you get a promise that is already
|
||||
* resolved with the specified value.
|
||||
*
|
||||
* @return A promise object where you can register completion callbacks to be
|
||||
* called when the task terminates.
|
||||
*/
|
||||
spawn: function (task) {
|
||||
return createAsyncFunction(task)();
|
||||
},
|
||||
|
||||
/**
|
||||
* Create and return an 'async function' that starts a new task.
|
||||
*
|
||||
* This is similar to 'spawn' except that it doesn't immediately start
|
||||
* the task, it binds the task to the async function's 'this' object and
|
||||
* arguments, and it requires the task to be a function.
|
||||
*
|
||||
* It simplifies the common pattern of implementing a method via a task,
|
||||
* like this simple object with a 'greet' method that has a 'name' parameter
|
||||
* and spawns a task to send a greeting and return its reply:
|
||||
*
|
||||
* let greeter = {
|
||||
* message: "Hello, NAME!",
|
||||
* greet: function(name) {
|
||||
* return Task.spawn((function* () {
|
||||
* return yield sendGreeting(this.message.replace(/NAME/, name));
|
||||
* }).bind(this);
|
||||
* })
|
||||
* };
|
||||
*
|
||||
* With Task.async, the method can be declared succinctly:
|
||||
*
|
||||
* let greeter = {
|
||||
* message: "Hello, NAME!",
|
||||
* greet: Task.async(function* (name) {
|
||||
* return yield sendGreeting(this.message.replace(/NAME/, name));
|
||||
* })
|
||||
* };
|
||||
*
|
||||
* While maintaining identical semantics:
|
||||
*
|
||||
* greeter.greet("Mitchell").then((reply) => { ... }); // behaves the same
|
||||
*
|
||||
* @param task
|
||||
* The task function to start.
|
||||
*
|
||||
* @return A function that starts the task function and returns its promise.
|
||||
*/
|
||||
async: function (task) {
|
||||
if (typeof (task) != "function") {
|
||||
throw new TypeError("task argument must be a function");
|
||||
}
|
||||
|
||||
return createAsyncFunction(task);
|
||||
},
|
||||
|
||||
/**
|
||||
* Constructs a special exception that, when thrown inside a legacy generator
|
||||
* function (non-star generator), allows the associated task to be resolved
|
||||
* with a specific value.
|
||||
*
|
||||
* Example: throw new Task.Result("Value");
|
||||
*/
|
||||
Result: function (value) {
|
||||
this.value = value;
|
||||
}
|
||||
};
|
||||
|
||||
function createAsyncFunction(task) {
|
||||
let asyncFunction = function () {
|
||||
let result = task;
|
||||
if (task && typeof (task) == "function") {
|
||||
if (task.isAsyncFunction) {
|
||||
throw new TypeError(
|
||||
"Cannot use an async function in place of a promise. " +
|
||||
"You should either invoke the async function first " +
|
||||
"or use 'Task.spawn' instead of 'Task.async' to start " +
|
||||
"the Task and return its promise.");
|
||||
}
|
||||
|
||||
try {
|
||||
// Let's call into the function ourselves.
|
||||
result = task.apply(this, arguments);
|
||||
} catch (ex) {
|
||||
if (ex instanceof Task.Result) {
|
||||
return Promise.resolve(ex.value);
|
||||
}
|
||||
return Promise.reject(ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (isGenerator(result)) {
|
||||
// This is an iterator resulting from calling a generator function.
|
||||
return new TaskImpl(result).deferred.promise;
|
||||
}
|
||||
|
||||
// Just propagate the given value to the caller as a resolved promise.
|
||||
return Promise.resolve(result);
|
||||
};
|
||||
|
||||
asyncFunction.isAsyncFunction = true;
|
||||
|
||||
return asyncFunction;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// TaskImpl
|
||||
|
||||
/**
|
||||
* Executes the specified iterator as a task, and gives access to the promise
|
||||
* that is fulfilled when the task terminates.
|
||||
*/
|
||||
function TaskImpl(iterator) {
|
||||
if (gMaintainStack) {
|
||||
this._stack = (new Error()).stack;
|
||||
}
|
||||
this.deferred = defer();
|
||||
this._iterator = iterator;
|
||||
this._isStarGenerator = !("send" in iterator);
|
||||
this._run(true);
|
||||
}
|
||||
|
||||
TaskImpl.prototype = {
|
||||
/**
|
||||
* Includes the promise object where task completion callbacks are registered,
|
||||
* and methods to resolve or reject the promise at task completion.
|
||||
*/
|
||||
deferred: null,
|
||||
|
||||
/**
|
||||
* The iterator returned by the generator function associated with this task.
|
||||
*/
|
||||
_iterator: null,
|
||||
|
||||
/**
|
||||
* Whether this Task is using a star generator.
|
||||
*/
|
||||
_isStarGenerator: false,
|
||||
|
||||
/**
|
||||
* Main execution routine, that calls into the generator function.
|
||||
*
|
||||
* @param sendResolved
|
||||
* If true, indicates that we should continue into the generator
|
||||
* function regularly (if we were waiting on a promise, it was
|
||||
* resolved). If true, indicates that we should cause an exception to
|
||||
* be thrown into the generator function (if we were waiting on a
|
||||
* promise, it was rejected).
|
||||
* @param sendValue
|
||||
* Resolution result or rejection exception, if any.
|
||||
*/
|
||||
_run: function (sendResolved, sendValue) {
|
||||
try {
|
||||
gCurrentTask = this;
|
||||
|
||||
if (this._isStarGenerator) {
|
||||
try {
|
||||
let result = sendResolved ? this._iterator.next(sendValue)
|
||||
: this._iterator.throw(sendValue);
|
||||
|
||||
if (result.done) {
|
||||
// The generator function returned.
|
||||
this.deferred.resolve(result.value);
|
||||
} else {
|
||||
// The generator function yielded.
|
||||
this._handleResultValue(result.value);
|
||||
}
|
||||
} catch (ex) {
|
||||
// The generator function failed with an uncaught exception.
|
||||
this._handleException(ex);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
let yielded = sendResolved ? this._iterator.send(sendValue)
|
||||
: this._iterator.throw(sendValue);
|
||||
this._handleResultValue(yielded);
|
||||
} catch (ex) {
|
||||
if (ex instanceof Task.Result) {
|
||||
// The generator function threw the special exception that
|
||||
// allows it to return a specific value on resolution.
|
||||
this.deferred.resolve(ex.value);
|
||||
} else if (ex instanceof StopIteration) {
|
||||
// The generator function terminated with no specific result.
|
||||
this.deferred.resolve(undefined);
|
||||
} else {
|
||||
// The generator function failed with an uncaught exception.
|
||||
this._handleException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
//
|
||||
// At this stage, the Task may have finished executing, or have
|
||||
// walked through a `yield` or passed control to a sub-Task.
|
||||
// Regardless, if we still own `gCurrentTask`, reset it. If we
|
||||
// have not finished execution of this Task, re-entering `_run`
|
||||
// will set `gCurrentTask` to `this` as needed.
|
||||
//
|
||||
// We just need to be careful here in case we hit the following
|
||||
// pattern:
|
||||
//
|
||||
// Task.spawn(foo);
|
||||
// Task.spawn(bar);
|
||||
//
|
||||
// Here, `foo` and `bar` may be interleaved, so when we finish
|
||||
// executing `foo`, `gCurrentTask` may actually either `foo` or
|
||||
// `bar`. If `gCurrentTask` has already been set to `bar`, leave
|
||||
// it be and it will be reset to `null` once `bar` is complete.
|
||||
//
|
||||
if (gCurrentTask == this) {
|
||||
gCurrentTask = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle a value yielded by a generator.
|
||||
*
|
||||
* @param value
|
||||
* The yielded value to handle.
|
||||
*/
|
||||
_handleResultValue: function (value) {
|
||||
// If our task yielded an iterator resulting from calling another
|
||||
// generator function, automatically spawn a task from it, effectively
|
||||
// turning it into a promise that is fulfilled on task completion.
|
||||
if (isGenerator(value)) {
|
||||
value = Task.spawn(value);
|
||||
}
|
||||
|
||||
if (value && typeof (value.then) == "function") {
|
||||
// We have a promise object now. When fulfilled, call again into this
|
||||
// function to continue the task, with either a resolution or rejection
|
||||
// condition.
|
||||
value.then(this._run.bind(this, true),
|
||||
this._run.bind(this, false));
|
||||
} else {
|
||||
// If our task yielded a value that is not a promise, just continue and
|
||||
// pass it directly as the result of the yield statement.
|
||||
this._run(true, value);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle an uncaught exception thrown from a generator.
|
||||
*
|
||||
* @param exception
|
||||
* The uncaught exception to handle.
|
||||
*/
|
||||
_handleException: function (exception) {
|
||||
gCurrentTask = this;
|
||||
|
||||
if (exception && typeof exception == "object" && "stack" in exception) {
|
||||
let stack = exception.stack;
|
||||
|
||||
if (gMaintainStack &&
|
||||
exception._capturedTaskStack != this._stack &&
|
||||
typeof stack == "string") {
|
||||
// Rewrite the stack for more readability.
|
||||
|
||||
let bottomStack = this._stack;
|
||||
|
||||
stack = Task.Debugging.generateReadableStack(stack);
|
||||
|
||||
exception.stack = stack;
|
||||
|
||||
// If exception is reinjected in the same task and rethrown,
|
||||
// we don't want to perform the rewrite again.
|
||||
exception._capturedTaskStack = bottomStack;
|
||||
} else if (!stack) {
|
||||
stack = "Not available";
|
||||
}
|
||||
|
||||
if ("name" in exception &&
|
||||
ERRORS_TO_REPORT.indexOf(exception.name) != -1) {
|
||||
// We suspect that the exception is a programmer error, so we now
|
||||
// display it using dump(). Note that we do not use Cu.reportError as
|
||||
// we assume that this is a programming error, so we do not want end
|
||||
// users to see it. Also, if the programmer handles errors correctly,
|
||||
// they will either treat the error or log them somewhere.
|
||||
|
||||
dump("*************************\n");
|
||||
dump("A coding exception was thrown and uncaught in a Task.\n\n");
|
||||
dump("Full message: " + exception + "\n");
|
||||
dump("Full stack: " + exception.stack + "\n");
|
||||
dump("*************************\n");
|
||||
}
|
||||
}
|
||||
|
||||
this.deferred.reject(exception);
|
||||
},
|
||||
|
||||
get callerStack() {
|
||||
// Cut `this._stack` at the last line of the first block that
|
||||
// contains task.js, keep the tail.
|
||||
for (let [line, index] of linesOf(this._stack || "")) {
|
||||
if (line.indexOf("/task.js:") == -1) {
|
||||
return this._stack.substring(index);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
Task.Debugging = {
|
||||
|
||||
/**
|
||||
* Control stack rewriting.
|
||||
*
|
||||
* If `true`, any exception thrown from a Task will be rewritten to
|
||||
* provide a human-readable stack trace. Otherwise, stack traces will
|
||||
* be left unchanged.
|
||||
*
|
||||
* There is a (small but existing) runtime cost associated to stack
|
||||
* rewriting, so you should probably not activate this in production
|
||||
* code.
|
||||
*
|
||||
* @type {bool}
|
||||
*/
|
||||
get maintainStack() {
|
||||
return gMaintainStack;
|
||||
},
|
||||
set maintainStack(x) {
|
||||
if (!x) {
|
||||
gCurrentTask = null;
|
||||
}
|
||||
gMaintainStack = x;
|
||||
return x;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate a human-readable stack for an error raised in
|
||||
* a Task.
|
||||
*
|
||||
* @param {string} topStack The stack provided by the error.
|
||||
* @param {string=} prefix Optionally, a prefix for each line.
|
||||
*/
|
||||
generateReadableStack: function (topStack, prefix = "") {
|
||||
if (!gCurrentTask) {
|
||||
return topStack;
|
||||
}
|
||||
|
||||
// Cut `topStack` at the first line that contains task.js, keep the head.
|
||||
let lines = [];
|
||||
for (let [line] of linesOf(topStack)) {
|
||||
if (line.indexOf("/task.js:") != -1) {
|
||||
break;
|
||||
}
|
||||
lines.push(prefix + line);
|
||||
}
|
||||
if (!prefix) {
|
||||
lines.push(gCurrentTask.callerStack);
|
||||
} else {
|
||||
for (let [line] of linesOf(gCurrentTask.callerStack)) {
|
||||
lines.push(prefix + line);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
};
|
||||
|
||||
exports.Task = Task;
|
|
@ -0,0 +1,331 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
|
||||
|
||||
/**
|
||||
* This is a stub of the DevTools telemetry module and will be replaced by the
|
||||
* full version of the file by Webpack for running inside Firefox.
|
||||
*/
|
||||
|
||||
class Telemetry {
|
||||
/**
|
||||
* Time since the system wide epoch. This is not a monotonic timer but
|
||||
* can be used across process boundaries.
|
||||
*/
|
||||
get msSystemNow() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a timer associated with a telemetry histogram. The timer can be
|
||||
* directly associated with a histogram, or with a pair of a histogram and
|
||||
* an object.
|
||||
*
|
||||
* @param {String} histogramId
|
||||
* A string which must be a valid histogram name.
|
||||
* @param {Object} obj
|
||||
* Optional parameter. If specified, the timer is associated with this
|
||||
* object, meaning that multiple timers for the same histogram may be
|
||||
* run concurrently, as long as they are associated with different
|
||||
* objects.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* True if the timer was successfully started, false otherwise. If a
|
||||
* timer already exists, it can't be started again, and the existing
|
||||
* one will be cleared in order to avoid measurements errors.
|
||||
*/
|
||||
start(histogramId, obj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a timer associated with a keyed telemetry histogram. The timer can
|
||||
* be directly associated with a histogram and its key. Similarly to
|
||||
* TelemetryStopwatch.start the histogram and its key can be associated
|
||||
* with an object. Each key may have multiple associated objects and each
|
||||
* object can be associated with multiple keys.
|
||||
*
|
||||
* @param {String} histogramId
|
||||
* A string which must be a valid histogram name.
|
||||
* @param {String} key
|
||||
* A string which must be a valid histgram key.
|
||||
* @param {Object} obj
|
||||
* Optional parameter. If specified, the timer is associated with this
|
||||
* object, meaning that multiple timers for the same histogram may be
|
||||
* run concurrently,as long as they are associated with different
|
||||
* objects.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* True if the timer was successfully started, false otherwise. If a
|
||||
* timer already exists, it can't be started again, and the existing
|
||||
* one will be cleared in order to avoid measurements errors.
|
||||
*/
|
||||
startKeyed(histogramId, key, obj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the timer associated with the given histogram (and object),
|
||||
* calculates the time delta between start and finish, and adds the value
|
||||
* to the histogram.
|
||||
*
|
||||
* @param {String} histogramId
|
||||
* A string which must be a valid histogram name.
|
||||
* @param {Object} obj
|
||||
* Optional parameter which associates the histogram timer with the
|
||||
* given object.
|
||||
* @param {Boolean} canceledOkay
|
||||
* Optional parameter which will suppress any warnings that normally
|
||||
* fire when a stopwatch is finished after being cancelled.
|
||||
* Defaults to false.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* True if the timer was succesfully stopped and the data was added
|
||||
* to the histogram, False otherwise.
|
||||
*/
|
||||
finish(histogramId, obj, canceledOkay) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the timer associated with the given keyed histogram (and object),
|
||||
* calculates the time delta between start and finish, and adds the value
|
||||
* to the keyed histogram.
|
||||
*
|
||||
* @param {String} histogramId
|
||||
* A string which must be a valid histogram name.
|
||||
* @param {String} key
|
||||
* A string which must be a valid histogram key.
|
||||
* @param {Object} obj
|
||||
* Optional parameter which associates the histogram timer with the
|
||||
* given object.
|
||||
* @param {Boolean} canceledOkay
|
||||
* Optional parameter which will suppress any warnings that normally
|
||||
* fire when a stopwatch is finished after being cancelled.
|
||||
* Defaults to false.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* True if the timer was succesfully stopped and the data was added
|
||||
* to the histogram, False otherwise.
|
||||
*/
|
||||
finishKeyed(histogramId, key, obj, cancelledOkay) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a value to a histogram.
|
||||
*
|
||||
* @param {String} histogramId
|
||||
* Histogram in which the data is to be stored.
|
||||
*/
|
||||
getHistogramById(histogramId) {
|
||||
return {
|
||||
add: () => {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a keyed histogram.
|
||||
*
|
||||
* @param {String} histogramId
|
||||
* Histogram in which the data is to be stored.
|
||||
*/
|
||||
getKeyedHistogramById(histogramId) {
|
||||
return {
|
||||
add: () => {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a value to a scalar.
|
||||
*
|
||||
* @param {String} scalarId
|
||||
* Scalar in which the data is to be stored.
|
||||
* @param value
|
||||
* Value to store.
|
||||
*/
|
||||
scalarSet(scalarId, value) {}
|
||||
|
||||
/**
|
||||
* Log a value to a count scalar.
|
||||
*
|
||||
* @param {String} scalarId
|
||||
* Scalar in which the data is to be stored.
|
||||
* @param value
|
||||
* Value to store.
|
||||
*/
|
||||
scalarAdd(scalarId, value) {}
|
||||
|
||||
/**
|
||||
* Log a value to a keyed count scalar.
|
||||
*
|
||||
* @param {String} scalarId
|
||||
* Scalar in which the data is to be stored.
|
||||
* @param {String} key
|
||||
* The key within the scalar.
|
||||
* @param value
|
||||
* Value to store.
|
||||
*/
|
||||
keyedScalarAdd(scalarId, key, value) {}
|
||||
|
||||
/**
|
||||
* Event telemetry is disabled by default. Use this method to enable it for
|
||||
* a particular category.
|
||||
*
|
||||
* @param {Boolean} enabled
|
||||
* Enabled: true or false.
|
||||
*/
|
||||
setEventRecordingEnabled(enabled) {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Telemetry events often need to make use of a number of properties from
|
||||
* completely different codepaths. To make this possible we create a
|
||||
* "pending event" along with an array of property names that we need to wait
|
||||
* for before sending the event.
|
||||
*
|
||||
* As each property is received via addEventProperty() we check if all
|
||||
* properties have been received. Once they have all been received we send the
|
||||
* telemetry event.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* The telemetry event or ping is associated with this object, meaning
|
||||
* that multiple events or pings for the same histogram may be run
|
||||
* concurrently, as long as they are associated with different objects.
|
||||
* @param {String} method
|
||||
* The telemetry event method (describes the type of event that
|
||||
* occurred e.g. "open")
|
||||
* @param {String} object
|
||||
* The telemetry event object name (the name of the object the event
|
||||
* occurred on) e.g. "tools" or "setting"
|
||||
* @param {String|null} value
|
||||
* The telemetry event value (a user defined value, providing context
|
||||
* for the event) e.g. "console"
|
||||
* @param {Array} expected
|
||||
* An array of the properties needed before sending the telemetry
|
||||
* event e.g.
|
||||
* [
|
||||
* "host",
|
||||
* "width"
|
||||
* ]
|
||||
*/
|
||||
preparePendingEvent(obj, method, object, value, expected = []) {}
|
||||
|
||||
/**
|
||||
* Adds an expected property for either a current or future pending event.
|
||||
* This means that if preparePendingEvent() is called before or after sending
|
||||
* the event properties they will automatically added to the event.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* The telemetry event or ping is associated with this object, meaning
|
||||
* that multiple events or pings for the same histogram may be run
|
||||
* concurrently, as long as they are associated with different objects.
|
||||
* @param {String} method
|
||||
* The telemetry event method (describes the type of event that
|
||||
* occurred e.g. "open")
|
||||
* @param {String} object
|
||||
* The telemetry event object name (the name of the object the event
|
||||
* occurred on) e.g. "tools" or "setting"
|
||||
* @param {String|null} value
|
||||
* The telemetry event value (a user defined value, providing context
|
||||
* for the event) e.g. "console"
|
||||
* @param {String} pendingPropName
|
||||
* The pending property name
|
||||
* @param {String} pendingPropValue
|
||||
* The pending property value
|
||||
*/
|
||||
addEventProperty(obj, method, object, value, pendingPropName, pendingPropValue) {}
|
||||
|
||||
/**
|
||||
* Adds expected properties for either a current or future pending event.
|
||||
* This means that if preparePendingEvent() is called before or after sending
|
||||
* the event properties they will automatically added to the event.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* The telemetry event or ping is associated with this object, meaning
|
||||
* that multiple events or pings for the same histogram may be run
|
||||
* concurrently, as long as they are associated with different objects.
|
||||
* @param {String} method
|
||||
* The telemetry event method (describes the type of event that
|
||||
* occurred e.g. "open")
|
||||
* @param {String} object
|
||||
* The telemetry event object name (the name of the object the event
|
||||
* occurred on) e.g. "tools" or "setting"
|
||||
* @param {String|null} value
|
||||
* The telemetry event value (a user defined value, providing context
|
||||
* for the event) e.g. "console"
|
||||
* @param {String} pendingObject
|
||||
* An object containing key, value pairs that should be added to the
|
||||
* event as properties.
|
||||
*/
|
||||
addEventProperties(obj, method, object, value, pendingObject) {}
|
||||
|
||||
/**
|
||||
* A private method that is not to be used externally. This method is used to
|
||||
* prepare a pending telemetry event for sending and then send it via
|
||||
* recordEvent().
|
||||
*
|
||||
* @param {Object} obj
|
||||
* The telemetry event or ping is associated with this object, meaning
|
||||
* that multiple events or pings for the same histogram may be run
|
||||
* concurrently, as long as they are associated with different objects.
|
||||
* @param {String} method
|
||||
* The telemetry event method (describes the type of event that
|
||||
* occurred e.g. "open")
|
||||
* @param {String} object
|
||||
* The telemetry event object name (the name of the object the event
|
||||
* occurred on) e.g. "tools" or "setting"
|
||||
* @param {String|null} value
|
||||
* The telemetry event value (a user defined value, providing context
|
||||
* for the event) e.g. "console"
|
||||
*/
|
||||
_sendPendingEvent(obj, method, object, value) {}
|
||||
|
||||
/**
|
||||
* Send a telemetry event.
|
||||
*
|
||||
* @param {String} method
|
||||
* The telemetry event method (describes the type of event that
|
||||
* occurred e.g. "open")
|
||||
* @param {String} object
|
||||
* The telemetry event object name (the name of the object the event
|
||||
* occurred on) e.g. "tools" or "setting"
|
||||
* @param {String|null} value
|
||||
* The telemetry event value (a user defined value, providing context
|
||||
* for the event) e.g. "console"
|
||||
* @param {Object} extra
|
||||
* The telemetry event extra object containing the properties that will
|
||||
* be sent with the event e.g.
|
||||
* {
|
||||
* host: "bottom",
|
||||
* width: "1024"
|
||||
* }
|
||||
*/
|
||||
recordEvent(method, object, value, extra) {}
|
||||
|
||||
/**
|
||||
* Sends telemetry pings to indicate that a tool has been opened.
|
||||
*
|
||||
* @param {String} id
|
||||
* The ID of the tool opened.
|
||||
* @param {String} sessionId
|
||||
* Toolbox session id used when we need to ensure a tool really has a
|
||||
* timer before calculating a delta.
|
||||
* @param {Object} obj
|
||||
* The telemetry event or ping is associated with this object, meaning
|
||||
* that multiple events or pings for the same histogram may be run
|
||||
* concurrently, as long as they are associated with different objects.
|
||||
*/
|
||||
toolOpened(id, sessionId, obj) {}
|
||||
|
||||
/**
|
||||
* Sends telemetry pings to indicate that a tool has been closed.
|
||||
*
|
||||
* @param {String} id
|
||||
* The ID of the tool opened.
|
||||
*/
|
||||
toolClosed(id, sessionId, obj) {}
|
||||
}
|
||||
|
||||
module.exports = Telemetry;
|
|
@ -0,0 +1,14 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Empty shim for "devtools/client/shared/zoom-keys" module
|
||||
*
|
||||
* Based on nsIMarkupDocumentViewer.fullZoom API
|
||||
* https://developer.mozilla.org/en-US/Firefox/Releases/3/Full_page_zoom
|
||||
*/
|
||||
exports.register = function (window) {
|
||||
};
|
|
@ -4673,14 +4673,6 @@ devtools-mc-assets@^0.0.8:
|
|||
resolved "https://registry.yarnpkg.com/devtools-mc-assets/-/devtools-mc-assets-0.0.8.tgz#47fc23240a916ed2c7d7bc6cc859882d49527e84"
|
||||
integrity sha512-+nvHMtga7sji1nCL+RaGyqZTfrmJtsdcsQIkI3VQuxvnRMcih59XcxzTrlEn9tQtQFLL0rNk5oqqbPfrKrCstA==
|
||||
|
||||
devtools-modules@~1.1.6, devtools-modules@~1.1.9:
|
||||
version "1.1.9"
|
||||
resolved "https://registry.yarnpkg.com/devtools-modules/-/devtools-modules-1.1.9.tgz#b095ff634229ae7f27fc1883757dbb5ca8b830db"
|
||||
integrity sha512-Gpwene1bA1vuLi4lu/1jqXZDjLijFCR/O08JXMcxls30dabctaav7XiU3mrg/bro/LrJoPxFro0OM1M7DRY0Vg==
|
||||
dependencies:
|
||||
devtools-services "0.0.1"
|
||||
punycode "^2.1.0"
|
||||
|
||||
devtools-services@0.0.1, devtools-services@^0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/devtools-services/-/devtools-services-0.0.1.tgz#9042600c11d1f4d45cc6ca299588a86fac1fbdd5"
|
||||
|
|
Загрузка…
Ссылка в новой задаче