mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-10-06 07:17:30 +09:00
Move Add-on SDK source to toolkit/jetpack
This commit is contained in:
parent
0a03067ee3
commit
23b67db438
993 changed files with 484 additions and 100663 deletions
11
toolkit/jetpack/app-extension/application.ini
Normal file
11
toolkit/jetpack/app-extension/application.ini
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[App]
|
||||
Vendor=Varma
|
||||
Name=Test App
|
||||
Version=1.0
|
||||
BuildID=20060101
|
||||
Copyright=Copyright (c) 2009 Atul Varma
|
||||
ID=xulapp@toolness.com
|
||||
|
||||
[Gecko]
|
||||
MinVersion=1.9.2.0
|
||||
MaxVersion=2.0.*
|
||||
362
toolkit/jetpack/app-extension/bootstrap.js
vendored
Normal file
362
toolkit/jetpack/app-extension/bootstrap.js
vendored
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
/* 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/. */
|
||||
|
||||
// @see http://dxr.mozilla.org/mozilla-central/source/js/src/xpconnect/loader/mozJSComponentLoader.cpp
|
||||
|
||||
'use strict';
|
||||
|
||||
// IMPORTANT: Avoid adding any initialization tasks here, if you need to do
|
||||
// something before add-on is loaded consider addon/runner module instead!
|
||||
|
||||
const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu,
|
||||
results: Cr, manager: Cm } = Components;
|
||||
const ioService = Cc['@mozilla.org/network/io-service;1'].
|
||||
getService(Ci.nsIIOService);
|
||||
const resourceHandler = ioService.getProtocolHandler('resource').
|
||||
QueryInterface(Ci.nsIResProtocolHandler);
|
||||
const systemPrincipal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')();
|
||||
const scriptLoader = Cc['@mozilla.org/moz/jssubscript-loader;1'].
|
||||
getService(Ci.mozIJSSubScriptLoader);
|
||||
const prefService = Cc['@mozilla.org/preferences-service;1'].
|
||||
getService(Ci.nsIPrefService).
|
||||
QueryInterface(Ci.nsIPrefBranch);
|
||||
const appInfo = Cc["@mozilla.org/xre/app-info;1"].
|
||||
getService(Ci.nsIXULAppInfo);
|
||||
const vc = Cc["@mozilla.org/xpcom/version-comparator;1"].
|
||||
getService(Ci.nsIVersionComparator);
|
||||
|
||||
const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
|
||||
|
||||
const Startup = Cu.import("resource://gre/modules/sdk/system/Startup.js", {}).exports;
|
||||
|
||||
|
||||
const REASON = [ 'unknown', 'startup', 'shutdown', 'enable', 'disable',
|
||||
'install', 'uninstall', 'upgrade', 'downgrade' ];
|
||||
|
||||
const bind = Function.call.bind(Function.bind);
|
||||
|
||||
var loader = null;
|
||||
var unload = null;
|
||||
var cuddlefishSandbox = null;
|
||||
var nukeTimer = null;
|
||||
|
||||
var resourceDomains = [];
|
||||
function setResourceSubstitution(domain, uri) {
|
||||
resourceDomains.push(domain);
|
||||
resourceHandler.setSubstitution(domain, uri);
|
||||
}
|
||||
|
||||
// Utility function that synchronously reads local resource from the given
|
||||
// `uri` and returns content string.
|
||||
function readURI(uri) {
|
||||
let channel = NetUtil.newChannel({
|
||||
uri: NetUtil.newURI(uri, 'UTF-8'),
|
||||
loadUsingSystemPrincipal: true
|
||||
});
|
||||
|
||||
let stream = channel.open2();
|
||||
|
||||
let cstream = Cc['@mozilla.org/intl/converter-input-stream;1'].
|
||||
createInstance(Ci.nsIConverterInputStream);
|
||||
cstream.init(stream, 'UTF-8', 0, 0);
|
||||
|
||||
let str = {};
|
||||
let data = '';
|
||||
let read = 0;
|
||||
do {
|
||||
read = cstream.readString(0xffffffff, str);
|
||||
data += str.value;
|
||||
} while (read != 0);
|
||||
|
||||
cstream.close();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// We don't do anything on install & uninstall yet, but in a future
|
||||
// we should allow add-ons to cleanup after uninstall.
|
||||
function install(data, reason) {}
|
||||
function uninstall(data, reason) {}
|
||||
|
||||
function startup(data, reasonCode) {
|
||||
try {
|
||||
let reason = REASON[reasonCode];
|
||||
// URI for the root of the XPI file.
|
||||
// 'jar:' URI if the addon is packed, 'file:' URI otherwise.
|
||||
// (Used by l10n module in order to fetch `locale` folder)
|
||||
let rootURI = data.resourceURI.spec;
|
||||
|
||||
// TODO: Maybe we should perform read harness-options.json asynchronously,
|
||||
// since we can't do anything until 'sessionstore-windows-restored' anyway.
|
||||
let options = JSON.parse(readURI(rootURI + './harness-options.json'));
|
||||
|
||||
let id = options.jetpackID;
|
||||
let name = options.name;
|
||||
|
||||
// Clean the metadata
|
||||
options.metadata[name]['permissions'] = options.metadata[name]['permissions'] || {};
|
||||
|
||||
// freeze the permissionss
|
||||
Object.freeze(options.metadata[name]['permissions']);
|
||||
// freeze the metadata
|
||||
Object.freeze(options.metadata[name]);
|
||||
|
||||
// Register a new resource 'domain' for this addon which is mapping to
|
||||
// XPI's `resources` folder.
|
||||
// Generate the domain name by using jetpack ID, which is the extension ID
|
||||
// by stripping common characters that doesn't work as a domain name:
|
||||
let uuidRe =
|
||||
/^\{([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\}$/;
|
||||
|
||||
let domain = id.
|
||||
toLowerCase().
|
||||
replace(/@/g, '-at-').
|
||||
replace(/\./g, '-dot-').
|
||||
replace(uuidRe, '$1');
|
||||
|
||||
let prefixURI = 'resource://' + domain + '/';
|
||||
let resourcesURI = ioService.newURI(rootURI + '/resources/', null, null);
|
||||
setResourceSubstitution(domain, resourcesURI);
|
||||
|
||||
// Create path to URLs mapping supported by loader.
|
||||
let paths = {
|
||||
// Relative modules resolve to add-on package lib
|
||||
'./': prefixURI + name + '/lib/',
|
||||
'./tests/': prefixURI + name + '/tests/',
|
||||
'': 'resource://gre/modules/commonjs/'
|
||||
};
|
||||
|
||||
// Maps addon lib and tests ressource folders for each package
|
||||
paths = Object.keys(options.metadata).reduce(function(result, name) {
|
||||
result[name + '/'] = prefixURI + name + '/lib/'
|
||||
result[name + '/tests/'] = prefixURI + name + '/tests/'
|
||||
return result;
|
||||
}, paths);
|
||||
|
||||
// We need to map tests folder when we run sdk tests whose package name
|
||||
// is stripped
|
||||
if (name == 'addon-sdk')
|
||||
paths['tests/'] = prefixURI + name + '/tests/';
|
||||
|
||||
let useBundledSDK = options['force-use-bundled-sdk'];
|
||||
if (!useBundledSDK) {
|
||||
try {
|
||||
useBundledSDK = prefService.getBoolPref("extensions.addon-sdk.useBundledSDK");
|
||||
}
|
||||
catch (e) {
|
||||
// Pref doesn't exist, allow using Firefox shipped SDK
|
||||
}
|
||||
}
|
||||
|
||||
// Starting with Firefox 21.0a1, we start using modules shipped into firefox
|
||||
// Still allow using modules from the xpi if the manifest tell us to do so.
|
||||
// And only try to look for sdk modules in xpi if the xpi actually ship them
|
||||
if (options['is-sdk-bundled'] &&
|
||||
(vc.compare(appInfo.version, '21.0a1') < 0 || useBundledSDK)) {
|
||||
// Maps sdk module folders to their resource folder
|
||||
paths[''] = prefixURI + 'addon-sdk/lib/';
|
||||
// test.js is usually found in root commonjs or SDK_ROOT/lib/ folder,
|
||||
// so that it isn't shipped in the xpi. Keep a copy of it in sdk/ folder
|
||||
// until we no longer support SDK modules in XPI:
|
||||
paths['test'] = prefixURI + 'addon-sdk/lib/sdk/test.js';
|
||||
}
|
||||
|
||||
// Retrieve list of module folder overloads based on preferences in order to
|
||||
// eventually used a local modules instead of files shipped into Firefox.
|
||||
let branch = prefService.getBranch('extensions.modules.' + id + '.path');
|
||||
paths = branch.getChildList('', {}).reduce(function (result, name) {
|
||||
// Allows overloading of any sub folder by replacing . by / in pref name
|
||||
let path = name.substr(1).split('.').join('/');
|
||||
// Only accept overloading folder by ensuring always ending with `/`
|
||||
if (path) path += '/';
|
||||
let fileURI = branch.getCharPref(name);
|
||||
|
||||
// On mobile, file URI has to end with a `/` otherwise, setSubstitution
|
||||
// takes the parent folder instead.
|
||||
if (fileURI[fileURI.length-1] !== '/')
|
||||
fileURI += '/';
|
||||
|
||||
// Maps the given file:// URI to a resource:// in order to avoid various
|
||||
// failure that happens with file:// URI and be close to production env
|
||||
let resourcesURI = ioService.newURI(fileURI, null, null);
|
||||
let resName = 'extensions.modules.' + domain + '.commonjs.path' + name;
|
||||
setResourceSubstitution(resName, resourcesURI);
|
||||
|
||||
result[path] = 'resource://' + resName + '/';
|
||||
return result;
|
||||
}, paths);
|
||||
|
||||
// Make version 2 of the manifest
|
||||
let manifest = options.manifest;
|
||||
|
||||
// Import `cuddlefish.js` module using a Sandbox and bootstrap loader.
|
||||
let cuddlefishPath = 'loader/cuddlefish.js';
|
||||
let cuddlefishURI = 'resource://gre/modules/commonjs/sdk/' + cuddlefishPath;
|
||||
if (paths['sdk/']) { // sdk folder has been overloaded
|
||||
// (from pref, or cuddlefish is still in the xpi)
|
||||
cuddlefishURI = paths['sdk/'] + cuddlefishPath;
|
||||
}
|
||||
else if (paths['']) { // root modules folder has been overloaded
|
||||
cuddlefishURI = paths[''] + 'sdk/' + cuddlefishPath;
|
||||
}
|
||||
|
||||
cuddlefishSandbox = loadSandbox(cuddlefishURI);
|
||||
let cuddlefish = cuddlefishSandbox.exports;
|
||||
|
||||
// Normalize `options.mainPath` so that it looks like one that will come
|
||||
// in a new version of linker.
|
||||
let main = options.mainPath;
|
||||
|
||||
unload = cuddlefish.unload;
|
||||
loader = cuddlefish.Loader({
|
||||
paths: paths,
|
||||
// modules manifest.
|
||||
manifest: manifest,
|
||||
|
||||
// Add-on ID used by different APIs as a unique identifier.
|
||||
id: id,
|
||||
// Add-on name.
|
||||
name: name,
|
||||
// Add-on version.
|
||||
version: options.metadata[name].version,
|
||||
// Add-on package descriptor.
|
||||
metadata: options.metadata[name],
|
||||
// Add-on load reason.
|
||||
loadReason: reason,
|
||||
|
||||
prefixURI: prefixURI,
|
||||
// Add-on URI.
|
||||
rootURI: rootURI,
|
||||
// options used by system module.
|
||||
// File to write 'OK' or 'FAIL' (exit code emulation).
|
||||
resultFile: options.resultFile,
|
||||
// Arguments passed as --static-args
|
||||
staticArgs: options.staticArgs,
|
||||
|
||||
// Option to prevent automatic kill of firefox during tests
|
||||
noQuit: options.no_quit,
|
||||
|
||||
// Add-on preferences branch name
|
||||
preferencesBranch: options.preferencesBranch,
|
||||
|
||||
// Arguments related to test runner.
|
||||
modules: {
|
||||
'@test/options': {
|
||||
iterations: options.iterations,
|
||||
filter: options.filter,
|
||||
profileMemory: options.profileMemory,
|
||||
stopOnError: options.stopOnError,
|
||||
verbose: options.verbose,
|
||||
parseable: options.parseable,
|
||||
checkMemory: options.check_memory,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let module = cuddlefish.Module('sdk/loader/cuddlefish', cuddlefishURI);
|
||||
let require = cuddlefish.Require(loader, module);
|
||||
|
||||
// Init the 'sdk/webextension' module from the bootstrap addon parameter.
|
||||
require("sdk/webextension").initFromBootstrapAddonParam(data);
|
||||
|
||||
require('sdk/addon/runner').startup(reason, {
|
||||
loader: loader,
|
||||
main: main,
|
||||
prefsURI: rootURI + 'defaults/preferences/prefs.js'
|
||||
});
|
||||
} catch (error) {
|
||||
dump('Bootstrap error: ' +
|
||||
(error.message ? error.message : String(error)) + '\n' +
|
||||
(error.stack || error.fileName + ': ' + error.lineNumber) + '\n');
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
function loadSandbox(uri) {
|
||||
let proto = {
|
||||
sandboxPrototype: {
|
||||
loadSandbox: loadSandbox,
|
||||
ChromeWorker: ChromeWorker
|
||||
}
|
||||
};
|
||||
let sandbox = Cu.Sandbox(systemPrincipal, proto);
|
||||
// Create a fake commonjs environnement just to enable loading loader.js
|
||||
// correctly
|
||||
sandbox.exports = {};
|
||||
sandbox.module = { uri: uri, exports: sandbox.exports };
|
||||
sandbox.require = function (id) {
|
||||
if (id !== "chrome")
|
||||
throw new Error("Bootstrap sandbox `require` method isn't implemented.");
|
||||
|
||||
return Object.freeze({ Cc: Cc, Ci: Ci, Cu: Cu, Cr: Cr, Cm: Cm,
|
||||
CC: bind(CC, Components), components: Components,
|
||||
ChromeWorker: ChromeWorker });
|
||||
};
|
||||
scriptLoader.loadSubScript(uri, sandbox, 'UTF-8');
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
function unloadSandbox(sandbox) {
|
||||
if (Cu.getClassName(sandbox, true) == "Sandbox")
|
||||
Cu.nukeSandbox(sandbox);
|
||||
}
|
||||
|
||||
function setTimeout(callback, delay) {
|
||||
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
timer.initWithCallback({ notify: callback }, delay,
|
||||
Ci.nsITimer.TYPE_ONE_SHOT);
|
||||
return timer;
|
||||
}
|
||||
|
||||
function shutdown(data, reasonCode) {
|
||||
let reason = REASON[reasonCode];
|
||||
if (loader) {
|
||||
unload(loader, reason);
|
||||
unload = null;
|
||||
|
||||
// Don't waste time cleaning up if the application is shutting down
|
||||
if (reason != "shutdown") {
|
||||
// Avoid leaking all modules when something goes wrong with one particular
|
||||
// module. Do not clean it up immediatly in order to allow executing some
|
||||
// actions on addon disabling.
|
||||
// We need to keep a reference to the timer, otherwise it is collected
|
||||
// and won't ever fire.
|
||||
nukeTimer = setTimeout(nukeModules, 1000);
|
||||
|
||||
// Bug 944951 - bootstrap.js must remove the added resource: URIs on unload
|
||||
resourceDomains.forEach(domain => {
|
||||
resourceHandler.setSubstitution(domain, null);
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function nukeModules() {
|
||||
nukeTimer = null;
|
||||
// module objects store `exports` which comes from sandboxes
|
||||
// We should avoid keeping link to these object to avoid leaking sandboxes
|
||||
for (let key in loader.modules) {
|
||||
delete loader.modules[key];
|
||||
}
|
||||
// Direct links to sandboxes should be removed too
|
||||
for (let key in loader.sandboxes) {
|
||||
let sandbox = loader.sandboxes[key];
|
||||
delete loader.sandboxes[key];
|
||||
// Bug 775067: From FF17 we can kill all CCW from a given sandbox
|
||||
unloadSandbox(sandbox);
|
||||
}
|
||||
unloadSandbox(loader.sharedGlobalSandbox);
|
||||
loader = null;
|
||||
|
||||
// both `toolkit/loader` and `system/xul-app` are loaded as JSM's via
|
||||
// `cuddlefish.js`, and needs to be unloaded to avoid memory leaks, when
|
||||
// the addon is unload.
|
||||
|
||||
unloadSandbox(cuddlefishSandbox.loaderSandbox);
|
||||
|
||||
// Bug 764840: We need to unload cuddlefish otherwise it will stay alive
|
||||
// and keep a reference to this compartment.
|
||||
unloadSandbox(cuddlefishSandbox);
|
||||
cuddlefishSandbox = null;
|
||||
}
|
||||
33
toolkit/jetpack/app-extension/install.rdf
Normal file
33
toolkit/jetpack/app-extension/install.rdf
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0"?>
|
||||
<!-- 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/. -->
|
||||
|
||||
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
|
||||
<Description about="urn:mozilla:install-manifest">
|
||||
<em:id>xulapp@toolness.com</em:id>
|
||||
<em:version>1.0</em:version>
|
||||
<em:type>2</em:type>
|
||||
<em:bootstrap>true</em:bootstrap>
|
||||
<em:unpack>false</em:unpack>
|
||||
|
||||
<!-- Firefox -->
|
||||
<em:targetApplication>
|
||||
<Description>
|
||||
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
|
||||
<em:minVersion>26.0</em:minVersion>
|
||||
<em:maxVersion>30.0</em:maxVersion>
|
||||
</Description>
|
||||
</em:targetApplication>
|
||||
|
||||
<!-- Front End MetaData -->
|
||||
<em:name>Test App</em:name>
|
||||
<em:description>Harness for tests.</em:description>
|
||||
<em:creator>Mozilla Corporation</em:creator>
|
||||
<em:homepageURL></em:homepageURL>
|
||||
<em:optionsType></em:optionsType>
|
||||
<em:optionsURL></em:optionsURL>
|
||||
<em:updateURL></em:updateURL>
|
||||
</Description>
|
||||
</RDF>
|
||||
95
toolkit/jetpack/dev/debuggee.js
Normal file
95
toolkit/jetpack/dev/debuggee.js
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
const { Class } = require("../sdk/core/heritage");
|
||||
const { MessagePort, MessageChannel } = require("../sdk/messaging");
|
||||
const { require: devtoolsRequire } = Cu.import("resource://devtools/shared/Loader.jsm", {});
|
||||
const { DebuggerServer } = devtoolsRequire("devtools/server/main");
|
||||
|
||||
const outputs = new WeakMap();
|
||||
const inputs = new WeakMap();
|
||||
const targets = new WeakMap();
|
||||
const transports = new WeakMap();
|
||||
|
||||
const inputFor = port => inputs.get(port);
|
||||
const outputFor = port => outputs.get(port);
|
||||
const transportFor = port => transports.get(port);
|
||||
|
||||
const fromTarget = target => {
|
||||
const debuggee = new Debuggee();
|
||||
const { port1, port2 } = new MessageChannel();
|
||||
inputs.set(debuggee, port1);
|
||||
outputs.set(debuggee, port2);
|
||||
targets.set(debuggee, target);
|
||||
|
||||
return debuggee;
|
||||
};
|
||||
exports.fromTarget = fromTarget;
|
||||
|
||||
const Debuggee = Class({
|
||||
extends: MessagePort.prototype,
|
||||
close: function() {
|
||||
const server = transportFor(this);
|
||||
if (server) {
|
||||
transports.delete(this);
|
||||
server.close();
|
||||
}
|
||||
outputFor(this).close();
|
||||
},
|
||||
start: function() {
|
||||
const target = targets.get(this);
|
||||
if (target.isLocalTab) {
|
||||
// Since a remote protocol connection will be made, let's start the
|
||||
// DebuggerServer here, once and for all tools.
|
||||
if (!DebuggerServer.initialized) {
|
||||
DebuggerServer.init();
|
||||
DebuggerServer.addBrowserActors();
|
||||
}
|
||||
|
||||
transports.set(this, DebuggerServer.connectPipe());
|
||||
}
|
||||
// TODO: Implement support for remote connections (See Bug 980421)
|
||||
else {
|
||||
throw Error("Remote targets are not yet supported");
|
||||
}
|
||||
|
||||
// pipe messages send to the debuggee to an actual
|
||||
// server via remote debugging protocol transport.
|
||||
inputFor(this).addEventListener("message", ({data}) =>
|
||||
transportFor(this).send(data));
|
||||
|
||||
// pipe messages received from the remote debugging
|
||||
// server transport onto the this debuggee.
|
||||
transportFor(this).hooks = {
|
||||
onPacket: packet => inputFor(this).postMessage(packet),
|
||||
onClosed: () => inputFor(this).close()
|
||||
};
|
||||
|
||||
inputFor(this).start();
|
||||
outputFor(this).start();
|
||||
},
|
||||
postMessage: function(data) {
|
||||
return outputFor(this).postMessage(data);
|
||||
},
|
||||
get onmessage() {
|
||||
return outputFor(this).onmessage;
|
||||
},
|
||||
set onmessage(onmessage) {
|
||||
outputFor(this).onmessage = onmessage;
|
||||
},
|
||||
addEventListener: function(...args) {
|
||||
return outputFor(this).addEventListener(...args);
|
||||
},
|
||||
removeEventListener: function(...args) {
|
||||
return outputFor(this).removeEventListener(...args);
|
||||
}
|
||||
});
|
||||
exports.Debuggee = Debuggee;
|
||||
120
toolkit/jetpack/dev/frame-script.js
Normal file
120
toolkit/jetpack/dev/frame-script.js
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
/* 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";
|
||||
(function({content, sendSyncMessage, addMessageListener, sendAsyncMessage}) {
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const observerService = Cc["@mozilla.org/observer-service;1"]
|
||||
.getService(Ci.nsIObserverService);
|
||||
|
||||
const channels = new Map();
|
||||
const handles = new WeakMap();
|
||||
|
||||
// Takes remote port handle and creates a local one.
|
||||
// also set's up a messaging channel between them.
|
||||
// This is temporary workaround until Bug 914974 is fixed
|
||||
// and port can be transfered through message manager.
|
||||
const demarshal = (handle) => {
|
||||
if (handle.type === "MessagePort") {
|
||||
if (!channels.has(handle.id)) {
|
||||
const channel = new content.MessageChannel();
|
||||
channels.set(handle.id, channel);
|
||||
handles.set(channel.port1, handle);
|
||||
channel.port1.onmessage = onOutPort;
|
||||
}
|
||||
return channels.get(handle.id).port2;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const onOutPort = event => {
|
||||
const handle = handles.get(event.target);
|
||||
sendAsyncMessage("sdk/port/message", {
|
||||
port: handle,
|
||||
message: event.data
|
||||
});
|
||||
};
|
||||
|
||||
const onInPort = ({data}) => {
|
||||
const channel = channels.get(data.port.id);
|
||||
if (channel)
|
||||
channel.port1.postMessage(data.message);
|
||||
};
|
||||
|
||||
const onOutEvent = event =>
|
||||
sendSyncMessage("sdk/event/" + event.type,
|
||||
{ type: event.type,
|
||||
data: event.data });
|
||||
|
||||
const onInMessage = (message) => {
|
||||
const {type, data, origin, bubbles, cancelable, ports} = message.data;
|
||||
|
||||
const event = new content.MessageEvent(type, {
|
||||
bubbles: bubbles,
|
||||
cancelable: cancelable,
|
||||
data: data,
|
||||
origin: origin,
|
||||
target: content,
|
||||
source: content,
|
||||
ports: ports.map(demarshal)
|
||||
});
|
||||
content.dispatchEvent(event);
|
||||
};
|
||||
|
||||
const onReady = event => {
|
||||
channels.clear();
|
||||
};
|
||||
|
||||
addMessageListener("sdk/event/message", onInMessage);
|
||||
addMessageListener("sdk/port/message", onInPort);
|
||||
|
||||
const observer = {
|
||||
handleEvent: ({target, type}) => {
|
||||
observer.observe(target, type);
|
||||
},
|
||||
observe: (document, topic, data) => {
|
||||
// When frame associated with message manager is removed from document `docShell`
|
||||
// is set to `null` but observer is still kept alive. At this point accesing
|
||||
// `content.document` throws "can't access dead object" exceptions. In order to
|
||||
// avoid leaking observer and logged errors observer is going to be removed when
|
||||
// `docShell` is set to `null`.
|
||||
if (!docShell) {
|
||||
observerService.removeObserver(observer, topic);
|
||||
}
|
||||
else if (document === content.document) {
|
||||
if (topic.endsWith("-document-interactive")) {
|
||||
sendAsyncMessage("sdk/event/ready", {
|
||||
type: "ready",
|
||||
readyState: document.readyState,
|
||||
uri: document.documentURI
|
||||
});
|
||||
}
|
||||
if (topic.endsWith("-document-loaded")) {
|
||||
sendAsyncMessage("sdk/event/load", {
|
||||
type: "load",
|
||||
readyState: document.readyState,
|
||||
uri: document.documentURI
|
||||
});
|
||||
}
|
||||
if (topic === "unload") {
|
||||
channels.clear();
|
||||
sendAsyncMessage("sdk/event/unload", {
|
||||
type: "unload",
|
||||
readyState: "uninitialized",
|
||||
uri: document.documentURI
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
observerService.addObserver(observer, "content-document-interactive", false);
|
||||
observerService.addObserver(observer, "content-document-loaded", false);
|
||||
observerService.addObserver(observer, "chrome-document-interactive", false);
|
||||
observerService.addObserver(observer, "chrome-document-loaded", false);
|
||||
addEventListener("unload", observer, false);
|
||||
|
||||
})(this);
|
||||
259
toolkit/jetpack/dev/panel.js
Normal file
259
toolkit/jetpack/dev/panel.js
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
const { Class } = require("../sdk/core/heritage");
|
||||
const { curry } = require("../sdk/lang/functional");
|
||||
const { EventTarget } = require("../sdk/event/target");
|
||||
const { Disposable, setup, dispose } = require("../sdk/core/disposable");
|
||||
const { emit, off, setListeners } = require("../sdk/event/core");
|
||||
const { when } = require("../sdk/event/utils");
|
||||
const { getFrameElement } = require("../sdk/window/utils");
|
||||
const { contract, validate } = require("../sdk/util/contract");
|
||||
const { data: { url: resolve }} = require("../sdk/self");
|
||||
const { identify } = require("../sdk/ui/id");
|
||||
const { isLocalURL, URL } = require("../sdk/url");
|
||||
const { encode } = require("../sdk/base64");
|
||||
const { marshal, demarshal } = require("./ports");
|
||||
const { fromTarget } = require("./debuggee");
|
||||
const { removed } = require("../sdk/dom/events");
|
||||
const { id: addonID } = require("../sdk/self");
|
||||
const { viewFor } = require("../sdk/view/core");
|
||||
const { createView } = require("./panel/view");
|
||||
|
||||
const OUTER_FRAME_URI = module.uri.replace(/\.js$/, ".html");
|
||||
const FRAME_SCRIPT = module.uri.replace("/panel.js", "/frame-script.js");
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
|
||||
const makeID = name =>
|
||||
("dev-panel-" + addonID + "-" + name).
|
||||
split("/").join("-").
|
||||
split(".").join("-").
|
||||
split(" ").join("-").
|
||||
replace(/[^A-Za-z0-9_\-]/g, "");
|
||||
|
||||
|
||||
// Weak mapping between `Panel` instances and their frame's
|
||||
// `nsIMessageManager`.
|
||||
const managers = new WeakMap();
|
||||
// Return `nsIMessageManager` for the given `Panel` instance.
|
||||
const managerFor = x => managers.get(x);
|
||||
|
||||
// Weak mappinging between iframe's and their owner
|
||||
// `Panel` instances.
|
||||
const panels = new WeakMap();
|
||||
const panelFor = frame => panels.get(frame);
|
||||
|
||||
// Weak mapping between panels and debugees they're targeting.
|
||||
const debuggees = new WeakMap();
|
||||
const debuggeeFor = panel => debuggees.get(panel);
|
||||
|
||||
const frames = new WeakMap();
|
||||
const frameFor = panel => frames.get(panel);
|
||||
|
||||
const setAttributes = (node, attributes) => {
|
||||
for (var key in attributes)
|
||||
node.setAttribute(key, attributes[key]);
|
||||
};
|
||||
|
||||
const onStateChange = ({target, data}) => {
|
||||
const panel = panelFor(target);
|
||||
panel.readyState = data.readyState;
|
||||
emit(panel, data.type, { target: panel, type: data.type });
|
||||
};
|
||||
|
||||
// port event listener on the message manager that demarshalls
|
||||
// and forwards to the actual receiver. This is a workaround
|
||||
// until Bug 914974 is fixed.
|
||||
const onPortMessage = ({data, target}) => {
|
||||
const port = demarshal(target, data.port);
|
||||
if (port)
|
||||
port.postMessage(data.message);
|
||||
};
|
||||
|
||||
// When frame is removed from the toolbox destroy panel
|
||||
// associated with it to release all the resources.
|
||||
const onFrameRemove = frame => {
|
||||
panelFor(frame).destroy();
|
||||
};
|
||||
|
||||
const onFrameInited = frame => {
|
||||
frame.style.visibility = "visible";
|
||||
}
|
||||
|
||||
const inited = frame => new Promise(resolve => {
|
||||
const { messageManager } = frame.frameLoader;
|
||||
const listener = message => {
|
||||
messageManager.removeMessageListener("sdk/event/ready", listener);
|
||||
resolve(frame);
|
||||
};
|
||||
messageManager.addMessageListener("sdk/event/ready", listener);
|
||||
});
|
||||
|
||||
const getTarget = ({target}) => target;
|
||||
|
||||
const Panel = Class({
|
||||
extends: Disposable,
|
||||
implements: [EventTarget],
|
||||
get id() {
|
||||
return makeID(this.name || this.label);
|
||||
},
|
||||
readyState: "uninitialized",
|
||||
ready: function() {
|
||||
const { readyState } = this;
|
||||
const isReady = readyState === "complete" ||
|
||||
readyState === "interactive";
|
||||
return isReady ? Promise.resolve(this) :
|
||||
when(this, "ready").then(getTarget);
|
||||
},
|
||||
loaded: function() {
|
||||
const { readyState } = this;
|
||||
const isLoaded = readyState === "complete";
|
||||
return isLoaded ? Promise.resolve(this) :
|
||||
when(this, "load").then(getTarget);
|
||||
},
|
||||
unloaded: function() {
|
||||
const { readyState } = this;
|
||||
const isUninitialized = readyState === "uninitialized";
|
||||
return isUninitialized ? Promise.resolve(this) :
|
||||
when(this, "unload").then(getTarget);
|
||||
},
|
||||
postMessage: function(data, ports=[]) {
|
||||
const manager = managerFor(this);
|
||||
manager.sendAsyncMessage("sdk/event/message", {
|
||||
type: "message",
|
||||
bubbles: false,
|
||||
cancelable: false,
|
||||
data: data,
|
||||
origin: this.url,
|
||||
ports: ports.map(marshal(manager))
|
||||
});
|
||||
}
|
||||
});
|
||||
exports.Panel = Panel;
|
||||
|
||||
validate.define(Panel, contract({
|
||||
label: {
|
||||
is: ["string"],
|
||||
msg: "The `option.label` must be a provided"
|
||||
},
|
||||
tooltip: {
|
||||
is: ["string", "undefined"],
|
||||
msg: "The `option.tooltip` must be a string"
|
||||
},
|
||||
icon: {
|
||||
is: ["string"],
|
||||
map: x => x && resolve(x),
|
||||
ok: x => isLocalURL(x),
|
||||
msg: "The `options.icon` must be a valid local URI."
|
||||
},
|
||||
url: {
|
||||
map: x => resolve(x.toString()),
|
||||
is: ["string"],
|
||||
ok: x => isLocalURL(x),
|
||||
msg: "The `options.url` must be a valid local URI."
|
||||
},
|
||||
invertIconForLightTheme: {
|
||||
is: ["boolean", "undefined"],
|
||||
msg: "The `options.invertIconForLightTheme` must be a boolean."
|
||||
},
|
||||
invertIconForDarkTheme: {
|
||||
is: ["boolean", "undefined"],
|
||||
msg: "The `options.invertIconForDarkTheme` must be a boolean."
|
||||
}
|
||||
}));
|
||||
|
||||
setup.define(Panel, (panel, {window, toolbox, url}) => {
|
||||
// Hack: Given that iframe created by devtools API is no good for us,
|
||||
// we obtain original iframe and replace it with the one that has
|
||||
// desired configuration.
|
||||
const original = getFrameElement(window);
|
||||
const container = original.parentNode;
|
||||
original.remove();
|
||||
const frame = createView(panel, container.ownerDocument);
|
||||
|
||||
// Following modifications are a temporary workaround until Bug 1049188
|
||||
// is fixed.
|
||||
// Enforce certain iframe customizations regardless of users request.
|
||||
setAttributes(frame, {
|
||||
"id": original.id,
|
||||
"src": url,
|
||||
"flex": 1,
|
||||
"forceOwnRefreshDriver": "",
|
||||
"tooltip": "aHTMLTooltip"
|
||||
});
|
||||
frame.style.visibility = "hidden";
|
||||
frame.classList.add("toolbox-panel-iframe");
|
||||
// Inject iframe into designated node until add-on author decides
|
||||
// to inject it elsewhere instead.
|
||||
if (!frame.parentNode)
|
||||
container.appendChild(frame);
|
||||
|
||||
// associate view with a panel
|
||||
frames.set(panel, frame);
|
||||
|
||||
// associate panel model with a frame view.
|
||||
panels.set(frame, panel);
|
||||
|
||||
const debuggee = fromTarget(toolbox.target);
|
||||
// associate debuggee with a panel.
|
||||
debuggees.set(panel, debuggee);
|
||||
|
||||
|
||||
// Setup listeners for the frame message manager.
|
||||
const { messageManager } = frame.frameLoader;
|
||||
messageManager.addMessageListener("sdk/event/ready", onStateChange);
|
||||
messageManager.addMessageListener("sdk/event/load", onStateChange);
|
||||
messageManager.addMessageListener("sdk/event/unload", onStateChange);
|
||||
messageManager.addMessageListener("sdk/port/message", onPortMessage);
|
||||
messageManager.loadFrameScript(FRAME_SCRIPT, false);
|
||||
|
||||
managers.set(panel, messageManager);
|
||||
|
||||
// destroy panel if frame is removed.
|
||||
removed(frame).then(onFrameRemove);
|
||||
// show frame when it is initialized.
|
||||
inited(frame).then(onFrameInited);
|
||||
|
||||
|
||||
// set listeners if there are ones defined on the prototype.
|
||||
setListeners(panel, Object.getPrototypeOf(panel));
|
||||
|
||||
|
||||
panel.setup({ debuggee: debuggee });
|
||||
});
|
||||
|
||||
createView.define(Panel, (panel, document) => {
|
||||
const frame = document.createElement("iframe");
|
||||
setAttributes(frame, {
|
||||
"sandbox": "allow-scripts",
|
||||
// We end up using chrome iframe with forced message manager
|
||||
// as fixing a swapFrameLoader seemed like a giant task (see
|
||||
// Bug 1075490).
|
||||
"type": "chrome",
|
||||
"forcemessagemanager": true,
|
||||
"transparent": true,
|
||||
"seamless": "seamless",
|
||||
});
|
||||
return frame;
|
||||
});
|
||||
|
||||
dispose.define(Panel, function(panel) {
|
||||
debuggeeFor(panel).close();
|
||||
|
||||
debuggees.delete(panel);
|
||||
managers.delete(panel);
|
||||
frames.delete(panel);
|
||||
panel.readyState = "destroyed";
|
||||
panel.dispose();
|
||||
});
|
||||
|
||||
viewFor.define(Panel, frameFor);
|
||||
14
toolkit/jetpack/dev/panel/view.js
Normal file
14
toolkit/jetpack/dev/panel/view.js
Normal file
|
|
@ -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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const { method } = require("method/core");
|
||||
|
||||
const createView = method("dev/panel/view#createView");
|
||||
exports.createView = createView;
|
||||
64
toolkit/jetpack/dev/ports.js
Normal file
64
toolkit/jetpack/dev/ports.js
Normal file
|
|
@ -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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
// This module provides `marshal` and `demarshal` functions
|
||||
// that can be used to send MessagePort's over `nsIFrameMessageManager`
|
||||
// until Bug 914974 is fixed.
|
||||
|
||||
const { add, iterator } = require("../sdk/lang/weak-set");
|
||||
const { curry } = require("../sdk/lang/functional");
|
||||
|
||||
var id = 0;
|
||||
const ports = new WeakMap();
|
||||
|
||||
// Takes `nsIFrameMessageManager` and `MessagePort` instances
|
||||
// and returns a handle representing given `port`. Messages
|
||||
// received on given `port` will be forwarded to a message
|
||||
// manager under `sdk/port/message` and messages like:
|
||||
// { port: { type: "MessagePort", id: 2}, data: data }
|
||||
// Where id is an identifier associated with a given `port`
|
||||
// and `data` is an `event.data` received on port.
|
||||
const marshal = curry((manager, port) => {
|
||||
if (!ports.has(port)) {
|
||||
id = id + 1;
|
||||
const handle = {type: "MessagePort", id: id};
|
||||
// Bind id to the given port
|
||||
ports.set(port, handle);
|
||||
|
||||
// Obtain a weak reference to a port.
|
||||
add(exports, port);
|
||||
|
||||
port.onmessage = event => {
|
||||
manager.sendAsyncMessage("sdk/port/message", {
|
||||
port: handle,
|
||||
message: event.data
|
||||
});
|
||||
};
|
||||
|
||||
return handle;
|
||||
}
|
||||
return ports.get(port);
|
||||
});
|
||||
exports.marshal = marshal;
|
||||
|
||||
// Takes `nsIFrameMessageManager` instance and a handle returned
|
||||
// `marshal(manager, port)` returning a `port` that was passed
|
||||
// to it. Note that `port` may be GC-ed in which case returned
|
||||
// value will be `null`.
|
||||
const demarshal = curry((manager, {type, id}) => {
|
||||
if (type === "MessagePort") {
|
||||
for (let port of iterator(exports)) {
|
||||
if (id === ports.get(port).id)
|
||||
return port;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
exports.demarshal = demarshal;
|
||||
135
toolkit/jetpack/dev/theme.js
Normal file
135
toolkit/jetpack/dev/theme.js
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const { Class } = require("../sdk/core/heritage");
|
||||
const { EventTarget } = require("../sdk/event/target");
|
||||
const { Disposable, setup, dispose } = require("../sdk/core/disposable");
|
||||
const { contract, validate } = require("../sdk/util/contract");
|
||||
const { id: addonID } = require("../sdk/self");
|
||||
const { onEnable, onDisable } = require("dev/theme/hooks");
|
||||
const { isString, instanceOf, isFunction } = require("sdk/lang/type");
|
||||
const { add } = require("sdk/util/array");
|
||||
const { data } = require("../sdk/self");
|
||||
const { isLocalURL } = require("../sdk/url");
|
||||
|
||||
const makeID = name =>
|
||||
("dev-theme-" + addonID + (name ? "-" + name : "")).
|
||||
split(/[ . /]/).join("-").
|
||||
replace(/[^A-Za-z0-9_\-]/g, "");
|
||||
|
||||
const Theme = Class({
|
||||
extends: Disposable,
|
||||
implements: [EventTarget],
|
||||
|
||||
initialize: function(options) {
|
||||
this.name = options.name;
|
||||
this.label = options.label;
|
||||
this.styles = options.styles;
|
||||
|
||||
// Event handlers
|
||||
this.onEnable = options.onEnable;
|
||||
this.onDisable = options.onDisable;
|
||||
},
|
||||
get id() {
|
||||
return makeID(this.name || this.label);
|
||||
},
|
||||
setup: function() {
|
||||
// Any initialization steps done at the registration time.
|
||||
},
|
||||
getStyles: function() {
|
||||
if (!this.styles) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (isString(this.styles)) {
|
||||
if (isLocalURL(this.styles)) {
|
||||
return [data.url(this.styles)];
|
||||
}
|
||||
}
|
||||
|
||||
let result = [];
|
||||
for (let style of this.styles) {
|
||||
if (isString(style)) {
|
||||
if (isLocalURL(style)) {
|
||||
style = data.url(style);
|
||||
}
|
||||
add(result, style);
|
||||
} else if (instanceOf(style, Theme)) {
|
||||
result = result.concat(style.getStyles());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
getClassList: function() {
|
||||
let result = [];
|
||||
for (let style of this.styles) {
|
||||
if (instanceOf(style, Theme)) {
|
||||
result = result.concat(style.getClassList());
|
||||
}
|
||||
}
|
||||
|
||||
if (this.name) {
|
||||
add(result, this.name);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
exports.Theme = Theme;
|
||||
|
||||
// Initialization & dispose
|
||||
|
||||
setup.define(Theme, (theme) => {
|
||||
theme.classList = [];
|
||||
theme.setup();
|
||||
});
|
||||
|
||||
dispose.define(Theme, function(theme) {
|
||||
theme.dispose();
|
||||
});
|
||||
|
||||
// Validation
|
||||
|
||||
validate.define(Theme, contract({
|
||||
label: {
|
||||
is: ["string"],
|
||||
msg: "The `option.label` must be a provided"
|
||||
},
|
||||
}));
|
||||
|
||||
// Support theme events: apply and unapply the theme.
|
||||
|
||||
onEnable.define(Theme, (theme, {window, oldTheme}) => {
|
||||
if (isFunction(theme.onEnable)) {
|
||||
theme.onEnable(window, oldTheme);
|
||||
}
|
||||
});
|
||||
|
||||
onDisable.define(Theme, (theme, {window, newTheme}) => {
|
||||
if (isFunction(theme.onDisable)) {
|
||||
theme.onDisable(window, newTheme);
|
||||
}
|
||||
});
|
||||
|
||||
// Support for built-in themes
|
||||
|
||||
const LightTheme = Theme({
|
||||
name: "theme-light",
|
||||
styles: "chrome://devtools/skin/light-theme.css",
|
||||
});
|
||||
|
||||
const DarkTheme = Theme({
|
||||
name: "theme-dark",
|
||||
styles: "chrome://devtools/skin/dark-theme.css",
|
||||
});
|
||||
|
||||
exports.LightTheme = LightTheme;
|
||||
exports.DarkTheme = DarkTheme;
|
||||
17
toolkit/jetpack/dev/theme/hooks.js
Normal file
17
toolkit/jetpack/dev/theme/hooks.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const { method } = require("method/core");
|
||||
|
||||
const onEnable = method("dev/theme/hooks#onEnable");
|
||||
const onDisable = method("dev/theme/hooks#onDisable");
|
||||
|
||||
exports.onEnable = onEnable;
|
||||
exports.onDisable = onDisable;
|
||||
107
toolkit/jetpack/dev/toolbox.js
Normal file
107
toolkit/jetpack/dev/toolbox.js
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const { Cu, Cc, Ci } = require("chrome");
|
||||
const { Class } = require("../sdk/core/heritage");
|
||||
const { Disposable, setup } = require("../sdk/core/disposable");
|
||||
const { contract, validate } = require("../sdk/util/contract");
|
||||
const { each, pairs, values } = require("../sdk/util/sequence");
|
||||
const { onEnable, onDisable } = require("../dev/theme/hooks");
|
||||
|
||||
const { gDevTools } = Cu.import("resource://devtools/client/framework/gDevTools.jsm", {});
|
||||
|
||||
// This is temporary workaround to allow loading of the developer tools client - volcan
|
||||
// into a toolbox panel, this hack won't be necessary as soon as devtools patch will be
|
||||
// shipped in nightly, after which it can be removed. Bug 1038517
|
||||
const registerSDKURI = () => {
|
||||
const ioService = Cc['@mozilla.org/network/io-service;1']
|
||||
.getService(Ci.nsIIOService);
|
||||
const resourceHandler = ioService.getProtocolHandler("resource")
|
||||
.QueryInterface(Ci.nsIResProtocolHandler);
|
||||
|
||||
const uri = module.uri.replace("dev/toolbox.js", "");
|
||||
resourceHandler.setSubstitution("sdk", ioService.newURI(uri, null, null));
|
||||
};
|
||||
|
||||
registerSDKURI();
|
||||
|
||||
const Tool = Class({
|
||||
extends: Disposable,
|
||||
setup: function(params={}) {
|
||||
const { panels } = validate(this, params);
|
||||
const { themes } = validate(this, params);
|
||||
|
||||
this.panels = panels;
|
||||
this.themes = themes;
|
||||
|
||||
each(([key, Panel]) => {
|
||||
const { url, label, tooltip, icon, invertIconForLightTheme,
|
||||
invertIconForDarkTheme } = validate(Panel.prototype);
|
||||
const { id } = Panel.prototype;
|
||||
|
||||
gDevTools.registerTool({
|
||||
id: id,
|
||||
url: "about:blank",
|
||||
label: label,
|
||||
tooltip: tooltip,
|
||||
icon: icon,
|
||||
invertIconForLightTheme: invertIconForLightTheme,
|
||||
invertIconForDarkTheme: invertIconForDarkTheme,
|
||||
isTargetSupported: target => target.isLocalTab,
|
||||
build: (window, toolbox) => {
|
||||
const panel = new Panel();
|
||||
setup(panel, { window: window,
|
||||
toolbox: toolbox,
|
||||
url: url });
|
||||
|
||||
return panel.ready();
|
||||
}
|
||||
});
|
||||
}, pairs(panels));
|
||||
|
||||
each(([key, theme]) => {
|
||||
validate(theme);
|
||||
setup(theme);
|
||||
|
||||
gDevTools.registerTheme({
|
||||
id: theme.id,
|
||||
label: theme.label,
|
||||
stylesheets: theme.getStyles(),
|
||||
classList: theme.getClassList(),
|
||||
onApply: (window, oldTheme) => {
|
||||
onEnable(theme, { window: window,
|
||||
oldTheme: oldTheme });
|
||||
},
|
||||
onUnapply: (window, newTheme) => {
|
||||
onDisable(theme, { window: window,
|
||||
newTheme: newTheme });
|
||||
}
|
||||
});
|
||||
}, pairs(themes));
|
||||
},
|
||||
dispose: function() {
|
||||
each(Panel => gDevTools.unregisterTool(Panel.prototype.id),
|
||||
values(this.panels));
|
||||
|
||||
each(Theme => gDevTools.unregisterTheme(Theme.prototype.id),
|
||||
values(this.themes));
|
||||
}
|
||||
});
|
||||
|
||||
validate.define(Tool, contract({
|
||||
panels: {
|
||||
is: ["object", "undefined"]
|
||||
},
|
||||
themes: {
|
||||
is: ["object", "undefined"]
|
||||
}
|
||||
}));
|
||||
|
||||
exports.Tool = Tool;
|
||||
40
toolkit/jetpack/dev/utils.js
Normal file
40
toolkit/jetpack/dev/utils.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/* 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 { Cu } = require("chrome");
|
||||
const { gDevTools } = Cu.import("resource://devtools/client/framework/gDevTools.jsm", {});
|
||||
const { devtools } = Cu.import("resource://devtools/shared/Loader.jsm", {});
|
||||
|
||||
const { getActiveTab } = require("../sdk/tabs/utils");
|
||||
const { getMostRecentBrowserWindow } = require("../sdk/window/utils");
|
||||
|
||||
const targetFor = target => {
|
||||
target = target || getActiveTab(getMostRecentBrowserWindow());
|
||||
return devtools.TargetFactory.forTab(target);
|
||||
};
|
||||
|
||||
const getId = id => ((id.prototype && id.prototype.id) || id.id || id);
|
||||
|
||||
const getCurrentPanel = toolbox => toolbox.getCurrentPanel();
|
||||
exports.getCurrentPanel = getCurrentPanel;
|
||||
|
||||
const openToolbox = (id, tab) => {
|
||||
id = getId(id);
|
||||
return gDevTools.showToolbox(targetFor(tab), id);
|
||||
};
|
||||
exports.openToolbox = openToolbox;
|
||||
|
||||
const closeToolbox = tab => gDevTools.closeToolbox(targetFor(tab));
|
||||
exports.closeToolbox = closeToolbox;
|
||||
|
||||
const getToolbox = tab => gDevTools.getToolbox(targetFor(tab));
|
||||
exports.getToolbox = getToolbox;
|
||||
|
||||
const openToolboxPanel = (id, tab) => {
|
||||
id = getId(id);
|
||||
return gDevTools.showToolbox(targetFor(tab), id).then(getCurrentPanel);
|
||||
};
|
||||
exports.openToolboxPanel = openToolboxPanel;
|
||||
3848
toolkit/jetpack/dev/volcan.js
Normal file
3848
toolkit/jetpack/dev/volcan.js
Normal file
File diff suppressed because one or more lines are too long
5
toolkit/jetpack/diffpatcher/.travis.yml
Normal file
5
toolkit/jetpack/diffpatcher/.travis.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- 0.4
|
||||
- 0.5
|
||||
- 0.6
|
||||
14
toolkit/jetpack/diffpatcher/History.md
Normal file
14
toolkit/jetpack/diffpatcher/History.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Changes
|
||||
|
||||
## 1.0.1 / 2013-05-01
|
||||
|
||||
- Update method library version.
|
||||
|
||||
## 1.0.0 / 2012-11-09
|
||||
|
||||
- Test integration for browsers.
|
||||
- New method library.
|
||||
|
||||
## 0.0.1 / 2012-10-22
|
||||
|
||||
- Initial release
|
||||
18
toolkit/jetpack/diffpatcher/License.md
Normal file
18
toolkit/jetpack/diffpatcher/License.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
Copyright 2012 Irakli Gozalishvili. All rights reserved.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
70
toolkit/jetpack/diffpatcher/Readme.md
Normal file
70
toolkit/jetpack/diffpatcher/Readme.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# diffpatcher
|
||||
|
||||
[](http://travis-ci.org/Gozala/diffpatcher)
|
||||
|
||||
[](http://ci.testling.com/Gozala/diffpatcher)
|
||||
|
||||
Diffpatcher is a small library that lets you treat hashes as if they were
|
||||
git repositories.
|
||||
|
||||
## diff
|
||||
|
||||
Diff function that takes two hashes and returns delta hash.
|
||||
|
||||
```js
|
||||
var diff = require("diffpatcher/diff")
|
||||
|
||||
diff({ a: { b: 1 }, c: { d: 2 } }, // hash#1
|
||||
{ a: { e: 3 }, c: { d: 4 } }) // hash#2
|
||||
|
||||
// => { // delta
|
||||
// a: {
|
||||
// b: null, // -
|
||||
// e: 3 // +
|
||||
// },
|
||||
// c: {
|
||||
// d: 4 // ±
|
||||
// }
|
||||
// }
|
||||
```
|
||||
|
||||
As you can see from the example above `delta` makes no real distinction between
|
||||
proprety upadate and property addition. Try to think of additions as an update
|
||||
from `undefined` to whatever it's being updated to.
|
||||
|
||||
## patch
|
||||
|
||||
Patch fuction takes a `hash` and a `delta` and returns a new `hash` which is
|
||||
just like orginial but with delta applied to it. Let's apply delta from the
|
||||
previous example to the first hash from the same example
|
||||
|
||||
|
||||
```js
|
||||
var patch = require("diffpatcher/patch")
|
||||
|
||||
patch({ a: { b: 1 }, c: { d: 2 } }, // hash#1
|
||||
{ // delta
|
||||
a: {
|
||||
b: null, // -
|
||||
e: 3 // +
|
||||
},
|
||||
c: {
|
||||
d: 4 // ±
|
||||
}
|
||||
})
|
||||
|
||||
// => { a: { e: 3 }, c: { d: 4 } } // hash#2
|
||||
```
|
||||
|
||||
That's about it really, just diffing hashes and applying thes diffs on them.
|
||||
|
||||
|
||||
### rebase
|
||||
|
||||
And as Linus mentioned everything in git can be expressed with `rebase`, that
|
||||
also pretty much the case for `diffpatcher`. `rebase` takes `target` hash,
|
||||
and rebases `parent` onto it with `diff` applied.
|
||||
|
||||
## Install
|
||||
|
||||
npm install diffpatcher
|
||||
45
toolkit/jetpack/diffpatcher/diff.js
Normal file
45
toolkit/jetpack/diffpatcher/diff.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
"use strict";
|
||||
|
||||
var method = require("../method/core")
|
||||
|
||||
// Method is designed to work with data structures representing application
|
||||
// state. Calling it with a state should return object representing `delta`
|
||||
// that has being applied to a previous state to get to a current state.
|
||||
//
|
||||
// Example
|
||||
//
|
||||
// diff(state) // => { "item-id-1": { title: "some title" } "item-id-2": null }
|
||||
var diff = method("diff@diffpatcher")
|
||||
|
||||
// diff between `null` / `undefined` to any hash is a hash itself.
|
||||
diff.define(null, function(from, to) { return to })
|
||||
diff.define(undefined, function(from, to) { return to })
|
||||
diff.define(Object, function(from, to) {
|
||||
return calculate(from, to || {}) || {}
|
||||
})
|
||||
|
||||
function calculate(from, to) {
|
||||
var diff = {}
|
||||
var changes = 0
|
||||
Object.keys(from).forEach(function(key) {
|
||||
changes = changes + 1
|
||||
if (!(key in to) && from[key] != null) diff[key] = null
|
||||
else changes = changes - 1
|
||||
})
|
||||
Object.keys(to).forEach(function(key) {
|
||||
changes = changes + 1
|
||||
var previous = from[key]
|
||||
var current = to[key]
|
||||
if (previous === current) return (changes = changes - 1)
|
||||
if (typeof(current) !== "object") return diff[key] = current
|
||||
if (typeof(previous) !== "object") return diff[key] = current
|
||||
var delta = calculate(previous, current)
|
||||
if (delta) diff[key] = delta
|
||||
else changes = changes - 1
|
||||
})
|
||||
return changes ? diff : null
|
||||
}
|
||||
|
||||
diff.calculate = calculate
|
||||
|
||||
module.exports = diff
|
||||
5
toolkit/jetpack/diffpatcher/index.js
Normal file
5
toolkit/jetpack/diffpatcher/index.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
"use strict";
|
||||
|
||||
exports.diff = require("./diff")
|
||||
exports.patch = require("./patch")
|
||||
exports.rebase = require("./rebase")
|
||||
54
toolkit/jetpack/diffpatcher/package.json
Normal file
54
toolkit/jetpack/diffpatcher/package.json
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"name": "diffpatcher",
|
||||
"id": "diffpatcher",
|
||||
"version": "1.2.0",
|
||||
"description": "Utilities for diff-ing & patch-ing hashes",
|
||||
"keywords": [
|
||||
"diff", "patch", "rebase", "hash", "changes", "versions"
|
||||
],
|
||||
"author": "Irakli Gozalishvili <rfobic@gmail.com> (http://jeditoolkit.com)",
|
||||
"homepage": "https://github.com/Gozala/diffpatcher",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Gozala/diffpatcher.git",
|
||||
"web": "https://github.com/Gozala/diffpatcher"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "http://github.com/Gozala/diffpatcher/issues/"
|
||||
},
|
||||
"dependencies": {
|
||||
"method": "~2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"test": "~0.x.0",
|
||||
"phantomify": "~0.x.0",
|
||||
"retape": "~0.x.0",
|
||||
"tape": "~0.1.5"
|
||||
},
|
||||
"main": "./index.js",
|
||||
"scripts": {
|
||||
"test": "npm run test-node && npm run test-browser",
|
||||
"test-browser": "node ./node_modules/phantomify/bin/cmd.js ./test/common.js",
|
||||
"test-node": "node ./test/common.js",
|
||||
"test-tap": "node ./test/tap.js"
|
||||
},
|
||||
"testling": {
|
||||
"files": "test/tap.js",
|
||||
"browsers": [
|
||||
"ie/9..latest",
|
||||
"chrome/25..latest",
|
||||
"firefox/20..latest",
|
||||
"safari/6..latest",
|
||||
"opera/11.0..latest",
|
||||
"iphone/6..latest",
|
||||
"ipad/6..latest",
|
||||
"android-browser/4.2..latest"
|
||||
]
|
||||
},
|
||||
"licenses": [
|
||||
{
|
||||
"type": "MIT",
|
||||
"url": "https://github.com/Gozala/diffpatcher/License.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
21
toolkit/jetpack/diffpatcher/patch.js
Normal file
21
toolkit/jetpack/diffpatcher/patch.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
"use strict";
|
||||
|
||||
var method = require("../method/core")
|
||||
var rebase = require("./rebase")
|
||||
|
||||
// Method is designed to work with data structures representing application
|
||||
// state. Calling it with a state and delta should return object representing
|
||||
// new state, with changes in `delta` being applied to previous.
|
||||
//
|
||||
// ## Example
|
||||
//
|
||||
// patch(state, {
|
||||
// "item-id-1": { completed: false }, // update
|
||||
// "item-id-2": null // delete
|
||||
// })
|
||||
var patch = method("patch@diffpatcher")
|
||||
patch.define(Object, function patch(hash, delta) {
|
||||
return rebase({}, hash, delta)
|
||||
})
|
||||
|
||||
module.exports = patch
|
||||
36
toolkit/jetpack/diffpatcher/rebase.js
Normal file
36
toolkit/jetpack/diffpatcher/rebase.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
"use strict";
|
||||
|
||||
var nil = {}
|
||||
var owns = ({}).hasOwnProperty
|
||||
|
||||
function rebase(result, parent, delta) {
|
||||
var key, current, previous, update
|
||||
for (key in parent) {
|
||||
if (owns.call(parent, key)) {
|
||||
previous = parent[key]
|
||||
update = owns.call(delta, key) ? delta[key] : nil
|
||||
if (previous === null) continue
|
||||
else if (previous === void(0)) continue
|
||||
else if (update === null) continue
|
||||
else if (update === void(0)) continue
|
||||
else result[key] = previous
|
||||
}
|
||||
}
|
||||
for (key in delta) {
|
||||
if (owns.call(delta, key)) {
|
||||
update = delta[key]
|
||||
current = owns.call(result, key) ? result[key] : nil
|
||||
if (current === update) continue
|
||||
else if (update === null) continue
|
||||
else if (update === void(0)) continue
|
||||
else if (current === nil) result[key] = update
|
||||
else if (typeof(update) !== "object") result[key] = update
|
||||
else if (typeof(current) !== "object") result[key] = update
|
||||
else result[key]= rebase({}, current, update)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
module.exports = rebase
|
||||
3
toolkit/jetpack/diffpatcher/test/common.js
Normal file
3
toolkit/jetpack/diffpatcher/test/common.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
"use strict";
|
||||
|
||||
require("test").run(require("./index"))
|
||||
59
toolkit/jetpack/diffpatcher/test/diff.js
Normal file
59
toolkit/jetpack/diffpatcher/test/diff.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
"use strict";
|
||||
|
||||
var diff = require("../diff")
|
||||
|
||||
exports["test diff from null"] = function(assert) {
|
||||
var to = { a: 1, b: 2 }
|
||||
assert.equal(diff(null, to), to, "diff null to x returns x")
|
||||
assert.equal(diff(void(0), to), to, "diff undefined to x returns x")
|
||||
|
||||
}
|
||||
|
||||
exports["test diff to null"] = function(assert) {
|
||||
var from = { a: 1, b: 2 }
|
||||
assert.deepEqual(diff({ a: 1, b: 2 }, null),
|
||||
{ a: null, b: null },
|
||||
"diff x null returns x with all properties nullified")
|
||||
}
|
||||
|
||||
exports["test diff identical"] = function(assert) {
|
||||
assert.deepEqual(diff({}, {}), {}, "diff on empty objects is {}")
|
||||
|
||||
assert.deepEqual(diff({ a: 1, b: 2 }, { a: 1, b: 2 }), {},
|
||||
"if properties match diff is {}")
|
||||
|
||||
assert.deepEqual(diff({ a: 1, b: { c: { d: 3, e: 4 } } },
|
||||
{ a: 1, b: { c: { d: 3, e: 4 } } }), {},
|
||||
"diff between identical nested hashes is {}")
|
||||
|
||||
}
|
||||
|
||||
exports["test diff delete"] = function(assert) {
|
||||
assert.deepEqual(diff({ a: 1, b: 2 }, { b: 2 }), { a: null },
|
||||
"missing property is deleted")
|
||||
assert.deepEqual(diff({ a: 1, b: 2 }, { a: 2 }), { a: 2, b: null },
|
||||
"missing property is deleted another updated")
|
||||
assert.deepEqual(diff({ a: 1, b: 2 }, {}), { a: null, b: null },
|
||||
"missing propertes are deleted")
|
||||
assert.deepEqual(diff({ a: 1, b: { c: { d: 2 } } }, {}),
|
||||
{ a: null, b: null },
|
||||
"missing deep propertes are deleted")
|
||||
assert.deepEqual(diff({ a: 1, b: { c: { d: 2 } } }, { b: { c: {} } }),
|
||||
{ a: null, b: { c: { d: null } } },
|
||||
"missing nested propertes are deleted")
|
||||
}
|
||||
|
||||
exports["test add update"] = function(assert) {
|
||||
assert.deepEqual(diff({ a: 1, b: 2 }, { b: 2, c: 3 }), { a: null, c: 3 },
|
||||
"delete and add")
|
||||
assert.deepEqual(diff({ a: 1, b: 2 }, { a: 2, c: 3 }), { a: 2, b: null, c: 3 },
|
||||
"delete and adds")
|
||||
assert.deepEqual(diff({}, { a: 1, b: 2 }), { a: 1, b: 2 },
|
||||
"diff on empty objcet returns equivalen of to")
|
||||
assert.deepEqual(diff({ a: 1, b: { c: { d: 2 } } }, { d: 3 }),
|
||||
{ a: null, b: null, d: 3 },
|
||||
"missing deep propertes are deleted")
|
||||
assert.deepEqual(diff({ b: { c: {} }, d: null }, { a: 1, b: { c: { d: 2 } } }),
|
||||
{ a: 1, b: { c: { d: 2 } } },
|
||||
"missing nested propertes are deleted")
|
||||
}
|
||||
14
toolkit/jetpack/diffpatcher/test/index.js
Normal file
14
toolkit/jetpack/diffpatcher/test/index.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
"use strict";
|
||||
|
||||
var diff = require("../diff")
|
||||
var patch = require("../patch")
|
||||
|
||||
exports["test diff"] = require("./diff")
|
||||
exports["test patch"] = require("./patch")
|
||||
|
||||
exports["test patch(a, diff(a, b)) => b"] = function(assert) {
|
||||
var a = { a: { b: 1 }, c: { d: 2 } }
|
||||
var b = { a: { e: 3 }, c: { d: 4 } }
|
||||
|
||||
assert.deepEqual(patch(a, diff(a, b)), b, "patch(a, diff(a, b)) => b")
|
||||
}
|
||||
83
toolkit/jetpack/diffpatcher/test/patch.js
Normal file
83
toolkit/jetpack/diffpatcher/test/patch.js
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
"use strict";
|
||||
|
||||
var patch = require("../patch")
|
||||
|
||||
exports["test patch delete"] = function(assert) {
|
||||
var hash = { a: 1, b: 2 }
|
||||
|
||||
assert.deepEqual(patch(hash, { a: null }), { b: 2 }, "null removes property")
|
||||
}
|
||||
|
||||
exports["test patch delete with void"] = function(assert) {
|
||||
var hash = { a: 1, b: 2 }
|
||||
|
||||
assert.deepEqual(patch(hash, { a: void(0) }), { b: 2 },
|
||||
"void(0) removes property")
|
||||
}
|
||||
|
||||
exports["test patch delete missing"] = function(assert) {
|
||||
assert.deepEqual(patch({ a: 1, b: 2 }, { c: null }),
|
||||
{ a: 1, b: 2 },
|
||||
"null removes property if exists");
|
||||
|
||||
assert.deepEqual(patch({ a: 1, b: 2 }, { c: void(0) }),
|
||||
{ a: 1, b: 2 },
|
||||
"void removes property if exists");
|
||||
}
|
||||
|
||||
exports["test delete deleted"] = function(assert) {
|
||||
assert.deepEqual(patch({ a: null, b: 2, c: 3, d: void(0)},
|
||||
{ a: void(0), b: null, d: null }),
|
||||
{c: 3},
|
||||
"removed all existing and non existing");
|
||||
}
|
||||
|
||||
exports["test update deleted"] = function(assert) {
|
||||
assert.deepEqual(patch({ a: null, b: void(0), c: 3},
|
||||
{ a: { b: 2 } }),
|
||||
{ a: { b: 2 }, c: 3 },
|
||||
"replace deleted");
|
||||
}
|
||||
|
||||
exports["test patch delete with void"] = function(assert) {
|
||||
var hash = { a: 1, b: 2 }
|
||||
|
||||
assert.deepEqual(patch(hash, { a: void(0) }), { b: 2 },
|
||||
"void(0) removes property")
|
||||
}
|
||||
|
||||
|
||||
exports["test patch addition"] = function(assert) {
|
||||
var hash = { a: 1, b: 2 }
|
||||
|
||||
assert.deepEqual(patch(hash, { c: 3 }), { a: 1, b: 2, c: 3 },
|
||||
"new properties are added")
|
||||
}
|
||||
|
||||
exports["test patch addition"] = function(assert) {
|
||||
var hash = { a: 1, b: 2 }
|
||||
|
||||
assert.deepEqual(patch(hash, { c: 3 }), { a: 1, b: 2, c: 3 },
|
||||
"new properties are added")
|
||||
}
|
||||
|
||||
exports["test hash on itself"] = function(assert) {
|
||||
var hash = { a: 1, b: 2 }
|
||||
|
||||
assert.deepEqual(patch(hash, hash), hash,
|
||||
"applying hash to itself returns hash itself")
|
||||
}
|
||||
|
||||
exports["test patch with empty delta"] = function(assert) {
|
||||
var hash = { a: 1, b: 2 }
|
||||
|
||||
assert.deepEqual(patch(hash, {}), hash,
|
||||
"applying empty delta results in no changes")
|
||||
}
|
||||
|
||||
exports["test patch nested data"] = function(assert) {
|
||||
assert.deepEqual(patch({ a: { b: 1 }, c: { d: 2 } },
|
||||
{ a: { b: null, e: 3 }, c: { d: 4 } }),
|
||||
{ a: { e: 3 }, c: { d: 4 } },
|
||||
"nested structures can also be patched")
|
||||
}
|
||||
3
toolkit/jetpack/diffpatcher/test/tap.js
Normal file
3
toolkit/jetpack/diffpatcher/test/tap.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
"use strict";
|
||||
|
||||
require("retape")(require("./index"))
|
||||
27
toolkit/jetpack/framescript/FrameScriptManager.jsm
Normal file
27
toolkit/jetpack/framescript/FrameScriptManager.jsm
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/* 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 globalMM = Components.classes["@mozilla.org/globalmessagemanager;1"].
|
||||
getService(Components.interfaces.nsIMessageListenerManager);
|
||||
|
||||
// Load frame scripts from the same dir as this module.
|
||||
// Since this JSM will be loaded using require(), PATH will be
|
||||
// overridden while running tests, just like any other module.
|
||||
const PATH = __URI__.replace('framescript/FrameScriptManager.jsm', '');
|
||||
|
||||
// Builds a unique loader ID for this runtime. We prefix with the SDK path so
|
||||
// overriden versions of the SDK don't conflict
|
||||
var LOADER_ID = 0;
|
||||
this.getNewLoaderID = () => {
|
||||
return PATH + ":" + LOADER_ID++;
|
||||
}
|
||||
|
||||
const frame_script = function(contentFrame, PATH) {
|
||||
let { registerContentFrame } = Components.utils.import(PATH + 'framescript/content.jsm', {});
|
||||
registerContentFrame(contentFrame);
|
||||
}
|
||||
globalMM.loadFrameScript("data:,(" + frame_script.toString() + ")(this, " + JSON.stringify(PATH) + ");", true);
|
||||
|
||||
this.EXPORTED_SYMBOLS = ['getNewLoaderID'];
|
||||
94
toolkit/jetpack/framescript/content.jsm
Normal file
94
toolkit/jetpack/framescript/content.jsm
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/* 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 { utils: Cu, classes: Cc, interfaces: Ci } = Components;
|
||||
const { Services } = Cu.import('resource://gre/modules/Services.jsm');
|
||||
|
||||
const cpmm = Cc['@mozilla.org/childprocessmessagemanager;1'].
|
||||
getService(Ci.nsISyncMessageSender);
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["registerContentFrame"];
|
||||
|
||||
// This may be an overriden version of the SDK so use the PATH as a key for the
|
||||
// initial messages before we have a loaderID.
|
||||
const PATH = __URI__.replace('framescript/content.jsm', '');
|
||||
|
||||
const { Loader } = Cu.import(PATH + 'toolkit/loader.js', {});
|
||||
|
||||
// one Loader instance per addon (per @loader/options to be precise)
|
||||
var addons = new Map();
|
||||
|
||||
// Tell the parent that a new process is ready
|
||||
cpmm.sendAsyncMessage('sdk/remote/process/start', {
|
||||
modulePath: PATH
|
||||
});
|
||||
|
||||
// Load a child process module loader with the given loader options
|
||||
cpmm.addMessageListener('sdk/remote/process/load', ({ data: { modulePath, loaderID, options, reason } }) => {
|
||||
if (modulePath != PATH)
|
||||
return;
|
||||
|
||||
// During startup races can mean we get a second load message
|
||||
if (addons.has(loaderID))
|
||||
return;
|
||||
|
||||
options.waiveInterposition = true;
|
||||
|
||||
let loader = Loader.Loader(options);
|
||||
let addon = {
|
||||
loader,
|
||||
require: Loader.Require(loader, { id: 'LoaderHelper' }),
|
||||
}
|
||||
addons.set(loaderID, addon);
|
||||
|
||||
cpmm.sendAsyncMessage('sdk/remote/process/attach', {
|
||||
loaderID,
|
||||
processID: Services.appinfo.processID,
|
||||
isRemote: Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
|
||||
});
|
||||
|
||||
addon.child = addon.require('sdk/remote/child');
|
||||
|
||||
for (let contentFrame of frames.values())
|
||||
addon.child.registerContentFrame(contentFrame);
|
||||
});
|
||||
|
||||
// Unload a child process loader
|
||||
cpmm.addMessageListener('sdk/remote/process/unload', ({ data: { loaderID, reason } }) => {
|
||||
if (!addons.has(loaderID))
|
||||
return;
|
||||
|
||||
let addon = addons.get(loaderID);
|
||||
Loader.unload(addon.loader, reason);
|
||||
|
||||
// We want to drop the reference to the loader but never allow creating a new
|
||||
// loader with the same ID
|
||||
addons.set(loaderID, {});
|
||||
})
|
||||
|
||||
|
||||
var frames = new Set();
|
||||
|
||||
this.registerContentFrame = contentFrame => {
|
||||
contentFrame.addEventListener("unload", () => {
|
||||
unregisterContentFrame(contentFrame);
|
||||
}, false);
|
||||
|
||||
frames.add(contentFrame);
|
||||
|
||||
for (let addon of addons.values()) {
|
||||
if ("child" in addon)
|
||||
addon.child.registerContentFrame(contentFrame);
|
||||
}
|
||||
};
|
||||
|
||||
function unregisterContentFrame(contentFrame) {
|
||||
frames.delete(contentFrame);
|
||||
|
||||
for (let addon of addons.values()) {
|
||||
if ("child" in addon)
|
||||
addon.child.unregisterContentFrame(contentFrame);
|
||||
}
|
||||
}
|
||||
215
toolkit/jetpack/framescript/context-menu.js
Normal file
215
toolkit/jetpack/framescript/context-menu.js
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
/* 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 { query, constant, cache } = require("sdk/lang/functional");
|
||||
const { pairs, each, map, object } = require("sdk/util/sequence");
|
||||
const { nodeToMessageManager } = require("./util");
|
||||
|
||||
// Decorator function that takes `f` function and returns one that attempts
|
||||
// to run `f` with given arguments. In case of exception error is logged
|
||||
// and `fallback` is returned instead.
|
||||
const Try = (fn, fallback=null) => (...args) => {
|
||||
try {
|
||||
return fn(...args);
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
|
||||
// Decorator funciton that takes `f` function and returns one that returns
|
||||
// JSON cloned result of whatever `f` returns for given arguments.
|
||||
const JSONReturn = f => (...args) => JSON.parse(JSON.stringify(f(...args)));
|
||||
|
||||
const Null = constant(null);
|
||||
|
||||
// Table of readers mapped to field names they're going to be reading.
|
||||
const readers = Object.create(null);
|
||||
// Read function takes "contextmenu" event target `node` and returns table of
|
||||
// read field names mapped to appropriate values. Read uses above defined read
|
||||
// table to read data for all registered readers.
|
||||
const read = node =>
|
||||
object(...map(([id, read]) => [id, read(node, id)], pairs(readers)));
|
||||
|
||||
// Table of built-in readers, each takes a descriptor and returns a reader:
|
||||
// descriptor -> node -> JSON
|
||||
const parsers = Object.create(null)
|
||||
// Function takes a descriptor of the remotely defined reader and parsese it
|
||||
// to construct a local reader that's going to read out data from context menu
|
||||
// target.
|
||||
const parse = descriptor => {
|
||||
const parser = parsers[descriptor.category];
|
||||
if (!parser) {
|
||||
console.error("Unknown reader descriptor was received", descriptor, `"${descriptor.category}"`);
|
||||
return Null
|
||||
}
|
||||
return Try(parser(descriptor));
|
||||
}
|
||||
|
||||
// TODO: Test how chrome's mediaType behaves to try and match it's behavior.
|
||||
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
|
||||
// Firefox always creates a HTMLVideoElement when loading an ogg file
|
||||
// directly. If the media is actually audio, be smarter and provide a
|
||||
// context menu with audio operations.
|
||||
// Source: https://github.com/mozilla/gecko-dev/blob/28c2fca3753c5371643843fc2f2f205146b083b7/browser/base/content/nsContextMenu.js#L632-L637
|
||||
const isVideoLoadingAudio = node =>
|
||||
node.readyState >= node.HAVE_METADATA &&
|
||||
(node.videoWidth == 0 || node.videoHeight == 0)
|
||||
|
||||
const isVideo = node =>
|
||||
node instanceof node.ownerDocument.defaultView.HTMLVideoElement &&
|
||||
!isVideoLoadingAudio(node);
|
||||
|
||||
const isAudio = node => {
|
||||
const {HTMLVideoElement, HTMLAudioElement} = node.ownerDocument.defaultView;
|
||||
return node instanceof HTMLAudioElement ? true :
|
||||
node instanceof HTMLVideoElement ? isVideoLoadingAudio(node) :
|
||||
false;
|
||||
};
|
||||
|
||||
const isImage = ({namespaceURI, localName}) =>
|
||||
namespaceURI === HTML_NS && localName === "img" ? true :
|
||||
namespaceURI === XUL_NS && localName === "image" ? true :
|
||||
namespaceURI === SVG_NS && localName === "image" ? true :
|
||||
false;
|
||||
|
||||
parsers["reader/MediaType()"] = constant(node =>
|
||||
isImage(node) ? "image" :
|
||||
isAudio(node) ? "audio" :
|
||||
isVideo(node) ? "video" :
|
||||
null);
|
||||
|
||||
|
||||
const readLink = node =>
|
||||
node.namespaceURI === HTML_NS && node.localName === "a" ? node.href :
|
||||
readLink(node.parentNode);
|
||||
|
||||
parsers["reader/LinkURL()"] = constant(node =>
|
||||
node.matches("a, a *") ? readLink(node) : null);
|
||||
|
||||
// Reader that reads out `true` if "contextmenu" `event.target` matches
|
||||
// `descriptor.selector` and `false` if it does not.
|
||||
parsers["reader/SelectorMatch()"] = ({selector}) =>
|
||||
node => node.matches(selector);
|
||||
|
||||
// Accessing `selectionStart` and `selectionEnd` properties on non
|
||||
// editable input nodes throw exceptions, there for we need this util
|
||||
// function to guard us against them.
|
||||
const getInputSelection = node => {
|
||||
try {
|
||||
if ("selectionStart" in node && "selectionEnd" in node) {
|
||||
const {selectionStart, selectionEnd} = node;
|
||||
return {selectionStart, selectionEnd}
|
||||
}
|
||||
}
|
||||
catch(_) {}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Selection reader does not really cares about descriptor so it is
|
||||
// a constant function returning selection reader. Selection reader
|
||||
// returns string of the selected text or `null` if there is no selection.
|
||||
parsers["reader/Selection()"] = constant(node => {
|
||||
const selection = node.ownerDocument.getSelection();
|
||||
if (!selection.isCollapsed) {
|
||||
return selection.toString();
|
||||
}
|
||||
// If target node is editable (text, input, textarea, etc..) document does
|
||||
// not really handles selections there. There for we fallback to checking
|
||||
// `selectionStart` `selectionEnd` properties and if they are present we
|
||||
// extract selections manually from the `node.value`.
|
||||
else {
|
||||
const selection = getInputSelection(node);
|
||||
const isSelected = selection &&
|
||||
Number.isInteger(selection.selectionStart) &&
|
||||
Number.isInteger(selection.selectionEnd) &&
|
||||
selection.selectionStart !== selection.selectionEnd;
|
||||
return isSelected ? node.value.substring(selection.selectionStart,
|
||||
selection.selectionEnd) :
|
||||
null;
|
||||
}
|
||||
});
|
||||
|
||||
// Query reader just reads out properties from the node, so we just use `query`
|
||||
// utility function.
|
||||
parsers["reader/Query()"] = ({path}) => JSONReturn(query(path));
|
||||
// Attribute reader just reads attribute of the event target node.
|
||||
parsers["reader/Attribute()"] = ({name}) => node => node.getAttribute(name);
|
||||
|
||||
// Extractor reader defines generates a reader out of serialized function, who's
|
||||
// return value is JSON cloned. Note: We do know source will evaluate to function
|
||||
// as that's what we serialized on the other end, it's also ok if generated function
|
||||
// is going to throw as registered readers are wrapped in try catch to avoid breakting
|
||||
// unrelated readers.
|
||||
parsers["reader/Extractor()"] = ({source}) =>
|
||||
JSONReturn(new Function("return (" + source + ")")());
|
||||
|
||||
// If the context-menu target node or any of its ancestors is one of these,
|
||||
// Firefox uses a tailored context menu, and so the page context doesn't apply.
|
||||
// There for `reader/isPage()` will read `false` in that case otherwise it's going
|
||||
// to read `true`.
|
||||
const nonPageElements = ["a", "applet", "area", "button", "canvas", "object",
|
||||
"embed", "img", "input", "map", "video", "audio", "menu",
|
||||
"option", "select", "textarea", "[contenteditable=true]"];
|
||||
const nonPageSelector = nonPageElements.
|
||||
concat(nonPageElements.map(tag => `${tag} *`)).
|
||||
join(", ");
|
||||
|
||||
// Note: isPageContext implementation could have actually used SelectorMatch reader,
|
||||
// but old implementation was also checked for collapsed selection there for to keep
|
||||
// the behavior same we end up implementing a new reader.
|
||||
parsers["reader/isPage()"] = constant(node =>
|
||||
node.ownerDocument.defaultView.getSelection().isCollapsed &&
|
||||
!node.matches(nonPageSelector));
|
||||
|
||||
// Reads `true` if node is in an iframe otherwise returns true.
|
||||
parsers["reader/isFrame()"] = constant(node =>
|
||||
!!node.ownerDocument.defaultView.frameElement);
|
||||
|
||||
parsers["reader/isEditable()"] = constant(node => {
|
||||
const selection = getInputSelection(node);
|
||||
return selection ? !node.readOnly && !node.disabled : node.isContentEditable;
|
||||
});
|
||||
|
||||
|
||||
// TODO: Add some reader to read out tab id.
|
||||
|
||||
const onReadersUpdate = message => {
|
||||
each(([id, descriptor]) => {
|
||||
if (descriptor) {
|
||||
readers[id] = parse(descriptor);
|
||||
}
|
||||
else {
|
||||
delete readers[id];
|
||||
}
|
||||
}, pairs(message.data));
|
||||
};
|
||||
exports.onReadersUpdate = onReadersUpdate;
|
||||
|
||||
|
||||
const onContextMenu = event => {
|
||||
if (!event.defaultPrevented) {
|
||||
const manager = nodeToMessageManager(event.target);
|
||||
manager.sendSyncMessage("sdk/context-menu/read", read(event.target), readers);
|
||||
}
|
||||
};
|
||||
exports.onContextMenu = onContextMenu;
|
||||
|
||||
|
||||
const onContentFrame = (frame) => {
|
||||
// Listen for contextmenu events in on this frame.
|
||||
frame.addEventListener("contextmenu", onContextMenu);
|
||||
// Listen to registered reader changes and update registry.
|
||||
frame.addMessageListener("sdk/context-menu/readers", onReadersUpdate);
|
||||
|
||||
// Request table of readers (if this is loaded in a new process some table
|
||||
// changes may be missed, this is way to sync up).
|
||||
frame.sendAsyncMessage("sdk/context-menu/readers?");
|
||||
};
|
||||
exports.onContentFrame = onContentFrame;
|
||||
26
toolkit/jetpack/framescript/manager.js
Normal file
26
toolkit/jetpack/framescript/manager.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const mime = "application/javascript";
|
||||
const requireURI = module.uri.replace("framescript/manager.js",
|
||||
"toolkit/require.js");
|
||||
|
||||
const requireLoadURI = `data:${mime},this["Components"].utils.import("${requireURI}")`
|
||||
|
||||
// Loads module with given `id` into given `messageManager` via shared module loader. If `init`
|
||||
// string is passed, will call module export with that name and pass frame script environment
|
||||
// of the `messageManager` into it. Since module will load only once per process (which is
|
||||
// once for chrome proces & second for content process) it is useful to have an init function
|
||||
// to setup event listeners on each content frame.
|
||||
const loadModule = (messageManager, id, allowDelayed, init) => {
|
||||
const moduleLoadURI = `${requireLoadURI}.require("${id}")`
|
||||
const uri = init ? `${moduleLoadURI}.${init}(this)` : moduleLoadURI;
|
||||
messageManager.loadFrameScript(uri, allowDelayed);
|
||||
};
|
||||
exports.loadModule = loadModule;
|
||||
25
toolkit/jetpack/framescript/util.js
Normal file
25
toolkit/jetpack/framescript/util.js
Normal file
|
|
@ -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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
|
||||
const { Ci } = require("chrome");
|
||||
|
||||
const windowToMessageManager = window =>
|
||||
window.
|
||||
QueryInterface(Ci.nsIInterfaceRequestor).
|
||||
getInterface(Ci.nsIDocShell).
|
||||
sameTypeRootTreeItem.
|
||||
QueryInterface(Ci.nsIDocShell).
|
||||
QueryInterface(Ci.nsIInterfaceRequestor).
|
||||
getInterface(Ci.nsIContentFrameMessageManager);
|
||||
exports.windowToMessageManager = windowToMessageManager;
|
||||
|
||||
const nodeToMessageManager = node =>
|
||||
windowToMessageManager(node.ownerDocument.defaultView);
|
||||
exports.nodeToMessageManager = nodeToMessageManager;
|
||||
3
toolkit/jetpack/index.js
Normal file
3
toolkit/jetpack/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
/* 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/. */
|
||||
53
toolkit/jetpack/jetpack-id/index.js
Normal file
53
toolkit/jetpack/jetpack-id/index.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Takes parsed `package.json` manifest and returns
|
||||
* valid add-on id for it.
|
||||
*/
|
||||
function getID(manifest) {
|
||||
manifest = manifest || {};
|
||||
|
||||
if (manifest.id) {
|
||||
|
||||
if (typeof manifest.id !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If manifest.id is already valid (as domain or GUID), use it
|
||||
if (isValidAOMName(manifest.id)) {
|
||||
return manifest.id;
|
||||
}
|
||||
// Otherwise, this ID is invalid so return `null`
|
||||
return null;
|
||||
}
|
||||
|
||||
// If no `id` defined, turn `name` into a domain ID,
|
||||
// as we transition to `name` being an id, similar to node/npm, but
|
||||
// append a '@' to make it compatible with Firefox requirements
|
||||
if (manifest.name) {
|
||||
|
||||
if (typeof manifest.name !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
var modifiedName = "@" + manifest.name;
|
||||
return isValidAOMName(modifiedName) ? modifiedName : null;
|
||||
}
|
||||
|
||||
// If no `id` or `name` property, return null as this manifest
|
||||
// is invalid
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = getID;
|
||||
|
||||
/**
|
||||
* Regex taken from XPIProvider.jsm in the Addon Manager to validate proper
|
||||
* IDs that are able to be used.
|
||||
* http://mxr.mozilla.org/mozilla-central/source/toolkit/mozapps/extensions/internal/XPIProvider.jsm#209
|
||||
*/
|
||||
function isValidAOMName (s) {
|
||||
return /^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}|[a-z0-9-\._]*\@[a-z0-9-\._]+)$/i.test(s || "");
|
||||
}
|
||||
28
toolkit/jetpack/jetpack-id/package.json
Normal file
28
toolkit/jetpack/jetpack-id/package.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "jetpack-id",
|
||||
"version": "1.0.0",
|
||||
"description": "Creates an ID from a Firefox Jetpack manifest",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/jsantell/jetpack-id"
|
||||
},
|
||||
"author": {
|
||||
"name": "Jordan Santell",
|
||||
"url": "http://github.com/jsantell"
|
||||
},
|
||||
"license": "MPL-2.0",
|
||||
"scripts": {
|
||||
"test": "./node_modules/.bin/mocha --reporter spec --ui bdd"
|
||||
},
|
||||
"keywords": [
|
||||
"jetpack",
|
||||
"addon",
|
||||
"mozilla",
|
||||
"firefox"
|
||||
],
|
||||
"devDependencies": {
|
||||
"mocha": "*",
|
||||
"chai": "*"
|
||||
}
|
||||
}
|
||||
5
toolkit/jetpack/method/.travis.yml
Normal file
5
toolkit/jetpack/method/.travis.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- 0.4
|
||||
- 0.5
|
||||
- 0.6
|
||||
55
toolkit/jetpack/method/History.md
Normal file
55
toolkit/jetpack/method/History.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# Changes
|
||||
|
||||
## 1.0.2 / 2012-12-26
|
||||
|
||||
- Delegate to polymorphic methods from `.define` and `.implement` so, they
|
||||
can be overidden.
|
||||
|
||||
## 1.0.1 / 2012-11-11
|
||||
|
||||
- Fix issues with different `Error` types as they all inherit from
|
||||
`Error`.
|
||||
|
||||
## 1.0.0 / 2012-11-09
|
||||
|
||||
- Add browser test integration.
|
||||
- Fix cross-browser incompatibilities & test failures.
|
||||
- Add support for host objects.
|
||||
- Add optional `hint` argument for method to ease debugging.
|
||||
- Remove default implementation at definition time.
|
||||
|
||||
## 0.1.1 / 2012-10-15
|
||||
|
||||
- Fix regression causing custom type implementation to be stored on objects.
|
||||
|
||||
## 0.1.0 / 2012-10-15
|
||||
|
||||
- Remove dependency on name module.
|
||||
- Implement fallback for engines that do not support ES5.
|
||||
- Add support for built-in type extensions without extending their prototypes.
|
||||
- Make API for default definitions more intuitive.
|
||||
Skipping type argument now defines default:
|
||||
|
||||
isFoo.define(function(value) {
|
||||
return false
|
||||
})
|
||||
|
||||
- Make exposed `define` and `implement` polymorphic.
|
||||
- Removed dev dependency on swank-js.
|
||||
- Primitive types `string, number, boolean` no longer inherit method
|
||||
implementations from `Object`.
|
||||
|
||||
## 0.0.3 / 2012-07-17
|
||||
|
||||
- Remove module boilerplate
|
||||
|
||||
## 0.0.2 / 2012-06-26
|
||||
|
||||
- Name changes to make it less conflicting with other library conventions.
|
||||
- Expose function version of `define` & `implement` methods.
|
||||
- Expose `Null` and `Undefined` object holding implementations for an
|
||||
associated types.
|
||||
|
||||
## 0.0.1 / 2012-06-25
|
||||
|
||||
- Initial release
|
||||
18
toolkit/jetpack/method/License.md
Normal file
18
toolkit/jetpack/method/License.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
Copyright 2012 Irakli Gozalishvili. All rights reserved.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
117
toolkit/jetpack/method/Readme.md
Normal file
117
toolkit/jetpack/method/Readme.md
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# method
|
||||
|
||||
[](http://travis-ci.org/Gozala/method)
|
||||
|
||||
Library provides an API for defining polymorphic methods that dispatch on the
|
||||
first argument type. This provides a powerful way for decouple abstraction
|
||||
interface definition from an actual implementation per type, without risks
|
||||
of interference with other libraries.
|
||||
|
||||
### Motivation
|
||||
|
||||
- Provide a high-performance, dynamic polymorphism construct as an
|
||||
alternative to existing object methods that does not provides any
|
||||
mechanics for guarding against name conflicts.
|
||||
- Allow independent extension of types, and implementations of methods
|
||||
on types, by different parties.
|
||||
|
||||
## Install
|
||||
|
||||
npm install method
|
||||
|
||||
## Use
|
||||
|
||||
```js
|
||||
var method = require("method")
|
||||
|
||||
// Define `isWatchable` method that can be implemented for any type.
|
||||
var isWatchable = method("isWatchable")
|
||||
|
||||
// If you call it on any object it will
|
||||
// throw as nothing implements that method yet.
|
||||
//isWatchable({}) // => Exception: method is not implemented
|
||||
|
||||
// If you define private method on `Object.prototype`
|
||||
// all objects will inherit it.
|
||||
Object.prototype[isWatchable] = function() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isWatchable({}) // => false
|
||||
|
||||
|
||||
// Although `isWatchable` property above will be enumerable and there for
|
||||
// may damage some assumbtions made by other libraries. There for it"s
|
||||
// recomended to use built-in helpers methods that will define extension
|
||||
// without breaking assumbtions made by other libraries:
|
||||
|
||||
isWatchable.define(Object, function() { return false })
|
||||
|
||||
|
||||
// There are primitive types in JS that won"t inherit methods from Object:
|
||||
isWatchable(null) // => Exception: method is not implemented
|
||||
|
||||
// One could either implement methods for such types:
|
||||
isWatchable.define(null, function() { return false })
|
||||
isWatchable.define(undefined, function() { return false })
|
||||
|
||||
// Or simply define default implementation:
|
||||
isWatchable.define(function() { return false })
|
||||
|
||||
// Alternatively default implementation may be provided at creation:
|
||||
isWatchable = method(function() { return false })
|
||||
|
||||
// Method dispatches on an first argument type. That allows us to create
|
||||
// new types with an alternative implementations:
|
||||
function Watchable() {}
|
||||
isWatchable.define(Watchable, function() { return true })
|
||||
|
||||
// This will make all `Watchable` instances watchable!
|
||||
isWatchable(new Watchable()) // => true
|
||||
|
||||
// Arbitrary objects can also be extended to implement given method. For example
|
||||
// any object can simply made watchable:
|
||||
function watchable(object) {
|
||||
return isWatchable.implement(objct, function() { return true })
|
||||
}
|
||||
|
||||
isWatchable(watchable({})) // => true
|
||||
|
||||
// Full protocols can be defined with such methods:
|
||||
var observers = "observers@" + module.filename
|
||||
var watchers = method("watchers")
|
||||
var watch = method("watch")
|
||||
var unwatch = method("unwatch")
|
||||
|
||||
watchers.define(Watchable, function(target) {
|
||||
return target[observers] || (target[observers] = [])
|
||||
})
|
||||
|
||||
watch.define(Watchable, function(target, watcher) {
|
||||
var observers = watchers(target)
|
||||
if (observers.indexOf(watcher) < 0) observers.push(watcher)
|
||||
return target
|
||||
})
|
||||
unwatch.define(Watchable, function(target, watcher) {
|
||||
var observers = watchers(target)
|
||||
var index = observers.indexOf(watcher)
|
||||
if (observers.indexOf(watcher) >= 0) observers.unshift(watcher)
|
||||
return target
|
||||
})
|
||||
|
||||
// Define type Port that inherits form Watchable
|
||||
|
||||
function Port() {}
|
||||
Port.prototype = Object.create(Watchable.prototype)
|
||||
|
||||
var emit = method("emit")
|
||||
emit.define(Port, function(port, message) {
|
||||
watchers(port).slice().forEach(function(watcher) {
|
||||
watcher(message)
|
||||
})
|
||||
})
|
||||
|
||||
var p = new Port()
|
||||
watch(p, console.log)
|
||||
emit(p, "hello world") // => info: "hello world"
|
||||
```
|
||||
225
toolkit/jetpack/method/core.js
Normal file
225
toolkit/jetpack/method/core.js
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
"use strict";
|
||||
|
||||
var defineProperty = Object.defineProperty || function(object, name, property) {
|
||||
object[name] = property.value
|
||||
return object
|
||||
}
|
||||
|
||||
// Shortcut for `Object.prototype.toString` for faster access.
|
||||
var typefy = Object.prototype.toString
|
||||
|
||||
// Map to for jumping from typeof(value) to associated type prefix used
|
||||
// as a hash in the map of builtin implementations.
|
||||
var types = { "function": "Object", "object": "Object" }
|
||||
|
||||
// Array is used to save method implementations for the host objects in order
|
||||
// to avoid extending them with non-primitive values that could cause leaks.
|
||||
var host = []
|
||||
// Hash map is used to save method implementations for builtin types in order
|
||||
// to avoid extending their prototypes. This also allows to share method
|
||||
// implementations for types across diff contexts / frames / compartments.
|
||||
var builtin = {}
|
||||
|
||||
function Primitive() {}
|
||||
function ObjectType() {}
|
||||
ObjectType.prototype = new Primitive()
|
||||
function ErrorType() {}
|
||||
ErrorType.prototype = new ObjectType()
|
||||
|
||||
var Default = builtin.Default = Primitive.prototype
|
||||
var Null = builtin.Null = new Primitive()
|
||||
var Void = builtin.Void = new Primitive()
|
||||
builtin.String = new Primitive()
|
||||
builtin.Number = new Primitive()
|
||||
builtin.Boolean = new Primitive()
|
||||
|
||||
builtin.Object = ObjectType.prototype
|
||||
builtin.Error = ErrorType.prototype
|
||||
|
||||
builtin.EvalError = new ErrorType()
|
||||
builtin.InternalError = new ErrorType()
|
||||
builtin.RangeError = new ErrorType()
|
||||
builtin.ReferenceError = new ErrorType()
|
||||
builtin.StopIteration = new ErrorType()
|
||||
builtin.SyntaxError = new ErrorType()
|
||||
builtin.TypeError = new ErrorType()
|
||||
builtin.URIError = new ErrorType()
|
||||
|
||||
|
||||
function Method(hint) {
|
||||
/**
|
||||
Private Method is a callable private name that dispatches on the first
|
||||
arguments same named Method:
|
||||
|
||||
method(object, ...rest) => object[method](...rest)
|
||||
|
||||
Optionally hint string may be provided that will be used in generated names
|
||||
to ease debugging.
|
||||
|
||||
## Example
|
||||
|
||||
var foo = Method()
|
||||
|
||||
// Implementation for any types
|
||||
foo.define(function(value, arg1, arg2) {
|
||||
// ...
|
||||
})
|
||||
|
||||
// Implementation for a specific type
|
||||
foo.define(BarType, function(bar, arg1, arg2) {
|
||||
// ...
|
||||
})
|
||||
**/
|
||||
|
||||
// Create an internal unique name if `hint` is provided it is used to
|
||||
// prefix name to ease debugging.
|
||||
var name = (hint || "") + "#" + Math.random().toString(32).substr(2)
|
||||
|
||||
function dispatch(value) {
|
||||
// Method dispatches on type of the first argument.
|
||||
// If first argument is `null` or `void` associated implementation is
|
||||
// looked up in the `builtin` hash where implementations for built-ins
|
||||
// are stored.
|
||||
var type = null
|
||||
var method = value === null ? Null[name] :
|
||||
value === void(0) ? Void[name] :
|
||||
// Otherwise attempt to use method with a generated private
|
||||
// `name` that is supposedly in the prototype chain of the
|
||||
// `target`.
|
||||
value[name] ||
|
||||
// Otherwise assume it's one of the built-in type instances,
|
||||
// in which case implementation is stored in a `builtin` hash.
|
||||
// Attempt to find a implementation for the given built-in
|
||||
// via constructor name and method name.
|
||||
((type = builtin[(value.constructor || "").name]) &&
|
||||
type[name]) ||
|
||||
// Otherwise assume it's a host object. For host objects
|
||||
// actual method implementations are stored in the `host`
|
||||
// array and only index for the implementation is stored
|
||||
// in the host object's prototype chain. This avoids memory
|
||||
// leaks that otherwise could happen when saving JS objects
|
||||
// on host object.
|
||||
host[value["!" + name] || void(0)] ||
|
||||
// Otherwise attempt to lookup implementation for builtins by
|
||||
// a type of the value. This basically makes sure that all
|
||||
// non primitive values will delegate to an `Object`.
|
||||
((type = builtin[types[typeof(value)]]) && type[name])
|
||||
|
||||
|
||||
// If method implementation for the type is still not found then
|
||||
// just fallback for default implementation.
|
||||
method = method || Default[name]
|
||||
|
||||
|
||||
// If implementation is still not found (which also means there is no
|
||||
// default) just throw an error with a descriptive message.
|
||||
if (!method) throw TypeError("Type does not implements method: " + name)
|
||||
|
||||
// If implementation was found then just delegate.
|
||||
return method.apply(method, arguments)
|
||||
}
|
||||
|
||||
// Make `toString` of the dispatch return a private name, this enables
|
||||
// method definition without sugar:
|
||||
//
|
||||
// var method = Method()
|
||||
// object[method] = function() { /***/ }
|
||||
dispatch.toString = function toString() { return name }
|
||||
|
||||
// Copy utility methods for convenient API.
|
||||
dispatch.implement = implementMethod
|
||||
dispatch.define = defineMethod
|
||||
|
||||
return dispatch
|
||||
}
|
||||
|
||||
// Create method shortcuts form functions.
|
||||
var defineMethod = function defineMethod(Type, lambda) {
|
||||
return define(this, Type, lambda)
|
||||
}
|
||||
var implementMethod = function implementMethod(object, lambda) {
|
||||
return implement(this, object, lambda)
|
||||
}
|
||||
|
||||
// Define `implement` and `define` polymorphic methods to allow definitions
|
||||
// and implementations through them.
|
||||
var implement = Method("implement")
|
||||
var define = Method("define")
|
||||
|
||||
|
||||
function _implement(method, object, lambda) {
|
||||
/**
|
||||
Implements `Method` for the given `object` with a provided `implementation`.
|
||||
Calling `Method` with `object` as a first argument will dispatch on provided
|
||||
implementation.
|
||||
**/
|
||||
return defineProperty(object, method.toString(), {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: lambda
|
||||
})
|
||||
}
|
||||
|
||||
function _define(method, Type, lambda) {
|
||||
/**
|
||||
Defines `Method` for the given `Type` with a provided `implementation`.
|
||||
Calling `Method` with a first argument of this `Type` will dispatch on
|
||||
provided `implementation`. If `Type` is a `Method` default implementation
|
||||
is defined. If `Type` is a `null` or `undefined` `Method` is implemented
|
||||
for that value type.
|
||||
**/
|
||||
|
||||
// Attempt to guess a type via `Object.prototype.toString.call` hack.
|
||||
var type = Type && typefy.call(Type.prototype)
|
||||
|
||||
// If only two arguments are passed then `Type` is actually an implementation
|
||||
// for a default type.
|
||||
if (!lambda) Default[method] = Type
|
||||
// If `Type` is `null` or `void` store implementation accordingly.
|
||||
else if (Type === null) Null[method] = lambda
|
||||
else if (Type === void(0)) Void[method] = lambda
|
||||
// If `type` hack indicates built-in type and type has a name us it to
|
||||
// store a implementation into associated hash. If hash for this type does
|
||||
// not exists yet create one.
|
||||
else if (type !== "[object Object]" && Type.name) {
|
||||
var Bulitin = builtin[Type.name] || (builtin[Type.name] = new ObjectType())
|
||||
Bulitin[method] = lambda
|
||||
}
|
||||
// If `type` hack indicates an object, that may be either object or any
|
||||
// JS defined "Class". If name of the constructor is `Object`, assume it's
|
||||
// built-in `Object` and store implementation accordingly.
|
||||
else if (Type.name === "Object")
|
||||
builtin.Object[method] = lambda
|
||||
// Host objects are pain!!! Every browser does some crazy stuff for them
|
||||
// So far all browser seem to not implement `call` method for host object
|
||||
// constructors. If that is a case here, assume it's a host object and
|
||||
// store implementation in a `host` array and store `index` in the array
|
||||
// in a `Type.prototype` itself. This avoids memory leaks that could be
|
||||
// caused by storing JS objects on a host objects.
|
||||
else if (Type.call === void(0)) {
|
||||
var index = host.indexOf(lambda)
|
||||
if (index < 0) index = host.push(lambda) - 1
|
||||
// Prefix private name with `!` so it can be dispatched from the method
|
||||
// without type checks.
|
||||
implement("!" + method, Type.prototype, index)
|
||||
}
|
||||
// If Got that far `Type` is user defined JS `Class`. Define private name
|
||||
// as hidden property on it's prototype.
|
||||
else
|
||||
implement(method, Type.prototype, lambda)
|
||||
}
|
||||
|
||||
// And provided implementations for a polymorphic equivalents.
|
||||
_define(define, _define)
|
||||
_define(implement, _implement)
|
||||
|
||||
// Define exports on `Method` as it's only thing being exported.
|
||||
Method.implement = implement
|
||||
Method.define = define
|
||||
Method.Method = Method
|
||||
Method.method = Method
|
||||
Method.builtin = builtin
|
||||
Method.host = host
|
||||
|
||||
module.exports = Method
|
||||
41
toolkit/jetpack/method/package.json
Normal file
41
toolkit/jetpack/method/package.json
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"name": "method",
|
||||
"id": "method",
|
||||
"version": "1.0.2",
|
||||
"description": "Functional polymorphic method dispatch",
|
||||
"keywords": [
|
||||
"method",
|
||||
"dispatch",
|
||||
"protocol",
|
||||
"polymorphism",
|
||||
"type dispatch"
|
||||
],
|
||||
"author": "Irakli Gozalishvili <rfobic@gmail.com> (http://jeditoolkit.com)",
|
||||
"homepage": "https://github.com/Gozala/method",
|
||||
"main": "./core.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Gozala/method.git",
|
||||
"web": "https://github.com/Gozala/method"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "http://github.com/Gozala/method/issues/"
|
||||
},
|
||||
"devDependencies": {
|
||||
"test": "~0.x.0",
|
||||
"repl-utils": "~2.0.1",
|
||||
"phantomify": "~0.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "npm run test-node && npm run test-browser",
|
||||
"test-browser": "node ./node_modules/phantomify/bin/cmd.js ./test/browser.js",
|
||||
"test-node": "node ./test/common.js",
|
||||
"repl": "node node_modules/repl-utils"
|
||||
},
|
||||
"licenses": [
|
||||
{
|
||||
"type": "MIT",
|
||||
"url": "https://github.com/Gozala/method/License.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
20
toolkit/jetpack/method/test/browser.js
Normal file
20
toolkit/jetpack/method/test/browser.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
"use strict";
|
||||
|
||||
exports["test common"] = require("./common")
|
||||
|
||||
var Method = require("../core")
|
||||
|
||||
exports["test host objects"] = function(assert) {
|
||||
var isElement = Method("is-element")
|
||||
isElement.define(function() { return false })
|
||||
|
||||
isElement.define(Element, function() { return true })
|
||||
|
||||
assert.notDeepEqual(typeof(Element.prototype[isElement]), "number",
|
||||
"Host object's prototype is extended with a number value")
|
||||
|
||||
assert.ok(!isElement({}), "object is not an Element")
|
||||
assert.ok(document.createElement("div"), "Element is an element")
|
||||
}
|
||||
|
||||
require("test").run(exports)
|
||||
272
toolkit/jetpack/method/test/common.js
Normal file
272
toolkit/jetpack/method/test/common.js
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
"use strict";
|
||||
|
||||
var Method = require("../core")
|
||||
|
||||
function type(value) {
|
||||
return Object.prototype.toString.call(value).
|
||||
split(" ").
|
||||
pop().
|
||||
split("]").
|
||||
shift().
|
||||
toLowerCase()
|
||||
}
|
||||
|
||||
var values = [
|
||||
null, // 0
|
||||
undefined, // 1
|
||||
Infinity, // 2
|
||||
NaN, // 3
|
||||
5, // 4
|
||||
{}, // 5
|
||||
Object.create({}), // 6
|
||||
Object.create(null), // 7
|
||||
[], // 8
|
||||
/foo/, // 9
|
||||
new Date(), // 10
|
||||
Function, // 11
|
||||
function() {}, // 12
|
||||
true, // 13
|
||||
false, // 14
|
||||
"string" // 15
|
||||
]
|
||||
|
||||
function True() { return true }
|
||||
function False() { return false }
|
||||
|
||||
var trues = values.map(True)
|
||||
var falses = values.map(False)
|
||||
|
||||
exports["test throws if not implemented"] = function(assert) {
|
||||
var method = Method("nope")
|
||||
|
||||
assert.throws(function() {
|
||||
method({})
|
||||
}, /not implement/i, "method throws if not implemented")
|
||||
|
||||
assert.throws(function() {
|
||||
method(null)
|
||||
}, /not implement/i, "method throws on null")
|
||||
}
|
||||
|
||||
exports["test all types inherit from default"] = function(assert) {
|
||||
var isImplemented = Method("isImplemented")
|
||||
isImplemented.define(function() { return true })
|
||||
|
||||
values.forEach(function(value) {
|
||||
assert.ok(isImplemented(value),
|
||||
type(value) + " inherits deafult implementation")
|
||||
})
|
||||
}
|
||||
|
||||
exports["test default can be implemented later"] = function(assert) {
|
||||
var isImplemented = Method("isImplemented")
|
||||
isImplemented.define(function() {
|
||||
return true
|
||||
})
|
||||
|
||||
values.forEach(function(value) {
|
||||
assert.ok(isImplemented(value),
|
||||
type(value) + " inherits deafult implementation")
|
||||
})
|
||||
}
|
||||
|
||||
exports["test dispatch not-implemented"] = function(assert) {
|
||||
var isDefault = Method("isDefault")
|
||||
values.forEach(function(value) {
|
||||
assert.throws(function() {
|
||||
isDefault(value)
|
||||
}, /not implement/, type(value) + " throws if not implemented")
|
||||
})
|
||||
}
|
||||
|
||||
exports["test dispatch default"] = function(assert) {
|
||||
var isDefault = Method("isDefault")
|
||||
|
||||
// Implement default
|
||||
isDefault.define(True)
|
||||
assert.deepEqual(values.map(isDefault), trues,
|
||||
"all implementation inherit from default")
|
||||
|
||||
}
|
||||
|
||||
exports["test dispatch null"] = function(assert) {
|
||||
var isNull = Method("isNull")
|
||||
|
||||
// Implement default
|
||||
isNull.define(False)
|
||||
isNull.define(null, True)
|
||||
assert.deepEqual(values.map(isNull),
|
||||
[ true ].
|
||||
concat(falses.slice(1)),
|
||||
"only null gets methods defined for null")
|
||||
}
|
||||
|
||||
exports["test dispatch undefined"] = function(assert) {
|
||||
var isUndefined = Method("isUndefined")
|
||||
|
||||
// Implement default
|
||||
isUndefined.define(False)
|
||||
isUndefined.define(undefined, True)
|
||||
assert.deepEqual(values.map(isUndefined),
|
||||
[ false, true ].
|
||||
concat(falses.slice(2)),
|
||||
"only undefined gets methods defined for undefined")
|
||||
}
|
||||
|
||||
exports["test dispatch object"] = function(assert) {
|
||||
var isObject = Method("isObject")
|
||||
|
||||
// Implement default
|
||||
isObject.define(False)
|
||||
isObject.define(Object, True)
|
||||
assert.deepEqual(values.map(isObject),
|
||||
[ false, false, false, false, false ].
|
||||
concat(trues.slice(5, 13)).
|
||||
concat([false, false, false]),
|
||||
"all values except primitives inherit Object methods")
|
||||
|
||||
}
|
||||
|
||||
exports["test dispatch number"] = function(assert) {
|
||||
var isNumber = Method("isNumber")
|
||||
isNumber.define(False)
|
||||
isNumber.define(Number, True)
|
||||
|
||||
assert.deepEqual(values.map(isNumber),
|
||||
falses.slice(0, 2).
|
||||
concat(true, true, true).
|
||||
concat(falses.slice(5)),
|
||||
"all numbers inherit from Number method")
|
||||
}
|
||||
|
||||
exports["test dispatch string"] = function(assert) {
|
||||
var isString = Method("isString")
|
||||
isString.define(False)
|
||||
isString.define(String, True)
|
||||
|
||||
assert.deepEqual(values.map(isString),
|
||||
falses.slice(0, 15).
|
||||
concat(true),
|
||||
"all strings inherit from String method")
|
||||
}
|
||||
|
||||
exports["test dispatch function"] = function(assert) {
|
||||
var isFunction = Method("isFunction")
|
||||
isFunction.define(False)
|
||||
isFunction.define(Function, True)
|
||||
|
||||
assert.deepEqual(values.map(isFunction),
|
||||
falses.slice(0, 11).
|
||||
concat(true, true).
|
||||
concat(falses.slice(13)),
|
||||
"all functions inherit from Function method")
|
||||
}
|
||||
|
||||
exports["test dispatch date"] = function(assert) {
|
||||
var isDate = Method("isDate")
|
||||
isDate.define(False)
|
||||
isDate.define(Date, True)
|
||||
|
||||
assert.deepEqual(values.map(isDate),
|
||||
falses.slice(0, 10).
|
||||
concat(true).
|
||||
concat(falses.slice(11)),
|
||||
"all dates inherit from Date method")
|
||||
}
|
||||
|
||||
exports["test dispatch RegExp"] = function(assert) {
|
||||
var isRegExp = Method("isRegExp")
|
||||
isRegExp.define(False)
|
||||
isRegExp.define(RegExp, True)
|
||||
|
||||
assert.deepEqual(values.map(isRegExp),
|
||||
falses.slice(0, 9).
|
||||
concat(true).
|
||||
concat(falses.slice(10)),
|
||||
"all regexps inherit from RegExp method")
|
||||
}
|
||||
|
||||
exports["test redefine for descendant"] = function(assert) {
|
||||
var isFoo = Method("isFoo")
|
||||
var ancestor = {}
|
||||
isFoo.implement(ancestor, function() { return true })
|
||||
var descendant = Object.create(ancestor)
|
||||
isFoo.implement(descendant, function() { return false })
|
||||
|
||||
assert.ok(isFoo(ancestor), "defined on ancestor")
|
||||
assert.ok(!isFoo(descendant), "overrided for descendant")
|
||||
}
|
||||
|
||||
exports["test on custom types"] = function(assert) {
|
||||
function Bar() {}
|
||||
var isBar = Method("isBar")
|
||||
|
||||
isBar.define(function() { return false })
|
||||
isBar.define(Bar, function() { return true })
|
||||
|
||||
assert.ok(!isBar({}), "object is get's default implementation")
|
||||
assert.ok(isBar(new Bar()), "Foo type objects get own implementation")
|
||||
|
||||
var isObject = Method("isObject")
|
||||
isObject.define(function() { return false })
|
||||
isObject.define(Object, function() { return true })
|
||||
|
||||
assert.ok(isObject(new Bar()), "foo inherits implementation from object")
|
||||
|
||||
|
||||
isObject.define(Bar, function() { return false })
|
||||
|
||||
assert.ok(!isObject(new Bar()),
|
||||
"implementation inherited form object can be overrided")
|
||||
}
|
||||
|
||||
|
||||
exports["test error types"] = function(assert) {
|
||||
var isError = Method("isError")
|
||||
isError.define(function() { return false })
|
||||
isError.define(Error, function() { return true })
|
||||
|
||||
assert.ok(isError(Error("boom")), "error is error")
|
||||
assert.ok(isError(TypeError("boom")), "type error is an error")
|
||||
assert.ok(isError(EvalError("boom")), "eval error is an error")
|
||||
assert.ok(isError(RangeError("boom")), "range error is an error")
|
||||
assert.ok(isError(ReferenceError("boom")), "reference error is an error")
|
||||
assert.ok(isError(SyntaxError("boom")), "syntax error is an error")
|
||||
assert.ok(isError(URIError("boom")), "URI error is an error")
|
||||
}
|
||||
|
||||
exports["test override define polymorphic method"] = function(assert) {
|
||||
var define = Method.define
|
||||
var implement = Method.implement
|
||||
|
||||
var fn = Method("fn")
|
||||
var methods = {}
|
||||
implement(define, fn, function(method, label, implementation) {
|
||||
methods[label] = implementation
|
||||
})
|
||||
|
||||
function foo() {}
|
||||
|
||||
define(fn, "foo-case", foo)
|
||||
|
||||
assert.equal(methods["foo-case"], foo, "define set property")
|
||||
}
|
||||
|
||||
exports["test override define via method API"] = function(assert) {
|
||||
var define = Method.define
|
||||
var implement = Method.implement
|
||||
|
||||
var fn = Method("fn")
|
||||
var methods = {}
|
||||
define.implement(fn, function(method, label, implementation) {
|
||||
methods[label] = implementation
|
||||
})
|
||||
|
||||
function foo() {}
|
||||
|
||||
define(fn, "foo-case", foo)
|
||||
|
||||
assert.equal(methods["foo-case"], foo, "define set property")
|
||||
}
|
||||
|
||||
require("test").run(exports)
|
||||
57
toolkit/jetpack/modules/system/Startup.js
Normal file
57
toolkit/jetpack/modules/system/Startup.js
Normal file
|
|
@ -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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Startup"];
|
||||
|
||||
var { utils: Cu, interfaces: Ci, classes: Cc } = Components;
|
||||
const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
|
||||
const { defer } = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
|
||||
|
||||
const { XulApp } = Cu.import("resource://gre/modules/commonjs/sdk/system/xul-app.jsm", {});
|
||||
|
||||
const appStartupSrv = Cc["@mozilla.org/toolkit/app-startup;1"]
|
||||
.getService(Ci.nsIAppStartup);
|
||||
|
||||
const NAME2TOPIC = {
|
||||
'Firefox': 'sessionstore-windows-restored',
|
||||
'Fennec': 'sessionstore-windows-restored',
|
||||
'SeaMonkey': 'sessionstore-windows-restored',
|
||||
'Thunderbird': 'mail-startup-done',
|
||||
'Instantbird': 'xul-window-visible'
|
||||
};
|
||||
|
||||
var Startup = {
|
||||
initialized: !appStartupSrv.startingUp
|
||||
};
|
||||
var exports = Startup;
|
||||
|
||||
var gOnceInitializedDeferred = defer();
|
||||
exports.onceInitialized = gOnceInitializedDeferred.promise;
|
||||
|
||||
// Set 'final-ui-startup' as default topic for unknown applications
|
||||
var appStartup = 'final-ui-startup';
|
||||
|
||||
if (Startup.initialized) {
|
||||
gOnceInitializedDeferred.resolve()
|
||||
}
|
||||
else {
|
||||
// Gets the topic that fit best as application startup event, in according with
|
||||
// the current application (e.g. Firefox, Fennec, Thunderbird...)
|
||||
for (let name of Object.keys(NAME2TOPIC)) {
|
||||
if (XulApp.is(name)) {
|
||||
appStartup = NAME2TOPIC[name];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let listener = function (subject, topic) {
|
||||
Services.obs.removeObserver(this, topic);
|
||||
Startup.initialized = true;
|
||||
Services.tm.currentThread.dispatch(() => gOnceInitializedDeferred.resolve(),
|
||||
Ci.nsIThread.DISPATCH_NORMAL);
|
||||
}
|
||||
|
||||
Services.obs.addObserver(listener, appStartup, false);
|
||||
}
|
||||
9
toolkit/jetpack/modules/system/moz.build
Normal file
9
toolkit/jetpack/modules/system/moz.build
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
EXTRA_JS_MODULES.sdk.system += [
|
||||
'Startup.js',
|
||||
]
|
||||
481
toolkit/jetpack/moz.build
Normal file
481
toolkit/jetpack/moz.build
Normal file
|
|
@ -0,0 +1,481 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
EXTRA_JS_MODULES.sdk += [
|
||||
'app-extension/bootstrap.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.sdk.system += [
|
||||
'modules/system/Startup.js',
|
||||
]
|
||||
|
||||
if CONFIG['MOZ_WIDGET_TOOLKIT'] != "gonk":
|
||||
EXTRA_JS_MODULES.commonjs.method.test += [
|
||||
'method/test/browser.js',
|
||||
'method/test/common.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.deprecated += [
|
||||
'sdk/deprecated/api-utils.js',
|
||||
'sdk/deprecated/sync-worker.js',
|
||||
'sdk/deprecated/unit-test-finder.js',
|
||||
'sdk/deprecated/unit-test.js',
|
||||
'sdk/deprecated/window-utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.frame += [
|
||||
'sdk/frame/hidden-frame.js',
|
||||
'sdk/frame/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.panel += [
|
||||
'sdk/panel/events.js',
|
||||
'sdk/panel/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.places += [
|
||||
'sdk/places/bookmarks.js',
|
||||
'sdk/places/contract.js',
|
||||
'sdk/places/events.js',
|
||||
'sdk/places/favicon.js',
|
||||
'sdk/places/history.js',
|
||||
'sdk/places/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.places.host += [
|
||||
'sdk/places/host/host-bookmarks.js',
|
||||
'sdk/places/host/host-query.js',
|
||||
'sdk/places/host/host-tags.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.tabs += [
|
||||
'sdk/tabs/common.js',
|
||||
'sdk/tabs/events.js',
|
||||
'sdk/tabs/helpers.js',
|
||||
'sdk/tabs/namespace.js',
|
||||
'sdk/tabs/observer.js',
|
||||
'sdk/tabs/tab-fennec.js',
|
||||
'sdk/tabs/tab-firefox.js',
|
||||
'sdk/tabs/tab.js',
|
||||
'sdk/tabs/tabs-firefox.js',
|
||||
'sdk/tabs/utils.js',
|
||||
'sdk/tabs/worker.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.test += [
|
||||
'sdk/test/assert.js',
|
||||
'sdk/test/harness.js',
|
||||
'sdk/test/httpd.js',
|
||||
'sdk/test/loader.js',
|
||||
'sdk/test/memory.js',
|
||||
'sdk/test/options.js',
|
||||
'sdk/test/runner.js',
|
||||
'sdk/test/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.ui += [
|
||||
'sdk/ui/component.js',
|
||||
'sdk/ui/frame.js',
|
||||
'sdk/ui/id.js',
|
||||
'sdk/ui/sidebar.js',
|
||||
'sdk/ui/state.js',
|
||||
'sdk/ui/toolbar.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.ui.button += [
|
||||
'sdk/ui/button/action.js',
|
||||
'sdk/ui/button/contract.js',
|
||||
'sdk/ui/button/toggle.js',
|
||||
'sdk/ui/button/view.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.ui.sidebar += [
|
||||
'sdk/ui/sidebar/actions.js',
|
||||
'sdk/ui/sidebar/contract.js',
|
||||
'sdk/ui/sidebar/namespace.js',
|
||||
'sdk/ui/sidebar/utils.js',
|
||||
'sdk/ui/sidebar/view.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.window += [
|
||||
'sdk/window/browser.js',
|
||||
'sdk/window/events.js',
|
||||
'sdk/window/helpers.js',
|
||||
'sdk/window/namespace.js',
|
||||
'sdk/window/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.windows += [
|
||||
'sdk/windows/fennec.js',
|
||||
'sdk/windows/firefox.js',
|
||||
'sdk/windows/observer.js',
|
||||
'sdk/windows/tabs-fennec.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs += [
|
||||
'index.js',
|
||||
'test.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk += [
|
||||
'sdk/webextension.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.dev += [
|
||||
'dev/debuggee.js',
|
||||
'dev/frame-script.js',
|
||||
'dev/panel.js',
|
||||
'dev/ports.js',
|
||||
'dev/theme.js',
|
||||
'dev/toolbox.js',
|
||||
'dev/utils.js',
|
||||
'dev/volcan.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.dev.panel += [
|
||||
'dev/panel/view.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.dev.theme += [
|
||||
'dev/theme/hooks.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.diffpatcher += [
|
||||
'diffpatcher/diff.js',
|
||||
'diffpatcher/index.js',
|
||||
'diffpatcher/patch.js',
|
||||
'diffpatcher/rebase.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.diffpatcher.test += [
|
||||
'diffpatcher/test/common.js',
|
||||
'diffpatcher/test/diff.js',
|
||||
'diffpatcher/test/index.js',
|
||||
'diffpatcher/test/patch.js',
|
||||
'diffpatcher/test/tap.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.framescript += [
|
||||
'framescript/content.jsm',
|
||||
'framescript/context-menu.js',
|
||||
'framescript/FrameScriptManager.jsm',
|
||||
'framescript/manager.js',
|
||||
'framescript/util.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs['jetpack-id'] += [
|
||||
'jetpack-id/index.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.method += [
|
||||
'method/core.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs['mozilla-toolkit-versioning'] += [
|
||||
'mozilla-toolkit-versioning/index.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs['mozilla-toolkit-versioning'].lib += [
|
||||
'mozilla-toolkit-versioning/lib/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.node += [
|
||||
'node/os.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk += [
|
||||
'sdk/base64.js',
|
||||
'sdk/clipboard.js',
|
||||
'sdk/context-menu.js',
|
||||
'sdk/context-menu@2.js',
|
||||
'sdk/hotkeys.js',
|
||||
'sdk/indexed-db.js',
|
||||
'sdk/l10n.js',
|
||||
'sdk/messaging.js',
|
||||
'sdk/notifications.js',
|
||||
'sdk/page-mod.js',
|
||||
'sdk/page-worker.js',
|
||||
'sdk/panel.js',
|
||||
'sdk/passwords.js',
|
||||
'sdk/private-browsing.js',
|
||||
'sdk/querystring.js',
|
||||
'sdk/request.js',
|
||||
'sdk/selection.js',
|
||||
'sdk/self.js',
|
||||
'sdk/simple-prefs.js',
|
||||
'sdk/simple-storage.js',
|
||||
'sdk/system.js',
|
||||
'sdk/tabs.js',
|
||||
'sdk/test.js',
|
||||
'sdk/timers.js',
|
||||
'sdk/ui.js',
|
||||
'sdk/url.js',
|
||||
'sdk/windows.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.addon += [
|
||||
'sdk/addon/bootstrap.js',
|
||||
'sdk/addon/events.js',
|
||||
'sdk/addon/host.js',
|
||||
'sdk/addon/installer.js',
|
||||
'sdk/addon/manager.js',
|
||||
'sdk/addon/runner.js',
|
||||
'sdk/addon/window.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.browser += [
|
||||
'sdk/browser/events.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.console += [
|
||||
'sdk/console/plain-text.js',
|
||||
'sdk/console/traceback.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.content += [
|
||||
'sdk/content/content-worker.js',
|
||||
'sdk/content/content.js',
|
||||
'sdk/content/context-menu.js',
|
||||
'sdk/content/events.js',
|
||||
'sdk/content/l10n-html.js',
|
||||
'sdk/content/loader.js',
|
||||
'sdk/content/mod.js',
|
||||
'sdk/content/page-mod.js',
|
||||
'sdk/content/page-worker.js',
|
||||
'sdk/content/sandbox.js',
|
||||
'sdk/content/tab-events.js',
|
||||
'sdk/content/thumbnail.js',
|
||||
'sdk/content/utils.js',
|
||||
'sdk/content/worker-child.js',
|
||||
'sdk/content/worker.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.content.sandbox += [
|
||||
'sdk/content/sandbox/events.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk['context-menu'] += [
|
||||
'sdk/context-menu/context.js',
|
||||
'sdk/context-menu/core.js',
|
||||
'sdk/context-menu/readers.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.core += [
|
||||
'sdk/core/disposable.js',
|
||||
'sdk/core/heritage.js',
|
||||
'sdk/core/namespace.js',
|
||||
'sdk/core/observer.js',
|
||||
'sdk/core/promise.js',
|
||||
'sdk/core/reference.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.deprecated.events += [
|
||||
'sdk/deprecated/events/assembler.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.dom += [
|
||||
'sdk/dom/events-shimmed.js',
|
||||
'sdk/dom/events.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.dom.events += [
|
||||
'sdk/dom/events/keys.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.event += [
|
||||
'sdk/event/chrome.js',
|
||||
'sdk/event/core.js',
|
||||
'sdk/event/dom.js',
|
||||
'sdk/event/target.js',
|
||||
'sdk/event/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.fs += [
|
||||
'sdk/fs/path.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.input += [
|
||||
'sdk/input/browser.js',
|
||||
'sdk/input/customizable-ui.js',
|
||||
'sdk/input/frame.js',
|
||||
'sdk/input/system.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.io += [
|
||||
'sdk/io/buffer.js',
|
||||
'sdk/io/byte-streams.js',
|
||||
'sdk/io/file.js',
|
||||
'sdk/io/fs.js',
|
||||
'sdk/io/stream.js',
|
||||
'sdk/io/text-streams.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.keyboard += [
|
||||
'sdk/keyboard/hotkeys.js',
|
||||
'sdk/keyboard/observer.js',
|
||||
'sdk/keyboard/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.l10n += [
|
||||
'sdk/l10n/core.js',
|
||||
'sdk/l10n/html.js',
|
||||
'sdk/l10n/loader.js',
|
||||
'sdk/l10n/locale.js',
|
||||
'sdk/l10n/plural-rules.js',
|
||||
'sdk/l10n/prefs.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.l10n.json += [
|
||||
'sdk/l10n/json/core.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.l10n.properties += [
|
||||
'sdk/l10n/properties/core.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.lang += [
|
||||
'sdk/lang/functional.js',
|
||||
'sdk/lang/type.js',
|
||||
'sdk/lang/weak-set.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.lang.functional += [
|
||||
'sdk/lang/functional/concurrent.js',
|
||||
'sdk/lang/functional/core.js',
|
||||
'sdk/lang/functional/helpers.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.loader += [
|
||||
'sdk/loader/cuddlefish.js',
|
||||
'sdk/loader/sandbox.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.model += [
|
||||
'sdk/model/core.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.net += [
|
||||
'sdk/net/url.js',
|
||||
'sdk/net/xhr.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.output += [
|
||||
'sdk/output/system.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk['page-mod'] += [
|
||||
'sdk/page-mod/match-pattern.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.passwords += [
|
||||
'sdk/passwords/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.platform += [
|
||||
'sdk/platform/xpcom.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.preferences += [
|
||||
'sdk/preferences/event-target.js',
|
||||
'sdk/preferences/native-options.js',
|
||||
'sdk/preferences/service.js',
|
||||
'sdk/preferences/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk['private-browsing'] += [
|
||||
'sdk/private-browsing/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.remote += [
|
||||
'sdk/remote/child.js',
|
||||
'sdk/remote/core.js',
|
||||
'sdk/remote/parent.js',
|
||||
'sdk/remote/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.stylesheet += [
|
||||
'sdk/stylesheet/style.js',
|
||||
'sdk/stylesheet/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.system += [
|
||||
'sdk/system/child_process.js',
|
||||
'sdk/system/environment.js',
|
||||
'sdk/system/events-shimmed.js',
|
||||
'sdk/system/events.js',
|
||||
'sdk/system/globals.js',
|
||||
'sdk/system/process.js',
|
||||
'sdk/system/runtime.js',
|
||||
'sdk/system/unload.js',
|
||||
'sdk/system/xul-app.js',
|
||||
'sdk/system/xul-app.jsm',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.system.child_process += [
|
||||
'sdk/system/child_process/subprocess.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.tab += [
|
||||
'sdk/tab/events.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.ui.button.view += [
|
||||
'sdk/ui/button/view/events.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.ui.frame += [
|
||||
'sdk/ui/frame/model.js',
|
||||
'sdk/ui/frame/view.html',
|
||||
'sdk/ui/frame/view.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.ui.state += [
|
||||
'sdk/ui/state/events.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.ui.toolbar += [
|
||||
'sdk/ui/toolbar/model.js',
|
||||
'sdk/ui/toolbar/view.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.uri += [
|
||||
'sdk/uri/resource.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.url += [
|
||||
'sdk/url/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.util += [
|
||||
'sdk/util/array.js',
|
||||
'sdk/util/collection.js',
|
||||
'sdk/util/contract.js',
|
||||
'sdk/util/deprecate.js',
|
||||
'sdk/util/dispatcher.js',
|
||||
'sdk/util/list.js',
|
||||
'sdk/util/match-pattern.js',
|
||||
'sdk/util/object.js',
|
||||
'sdk/util/rules.js',
|
||||
'sdk/util/sequence.js',
|
||||
'sdk/util/uuid.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.view += [
|
||||
'sdk/view/core.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.worker += [
|
||||
'sdk/worker/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.zip += [
|
||||
'sdk/zip/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.toolkit += [
|
||||
'toolkit/loader.js',
|
||||
'toolkit/require.js',
|
||||
]
|
||||
112
toolkit/jetpack/mozilla-toolkit-versioning/index.js
Normal file
112
toolkit/jetpack/mozilla-toolkit-versioning/index.js
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/* 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 versionParse = require('./lib/utils').versionParse;
|
||||
|
||||
var COMPARATORS = ['>=', '<=', '>', '<', '=', '~', '^'];
|
||||
|
||||
exports.parse = function (input) {
|
||||
input = input || '';
|
||||
input = input.trim();
|
||||
if (!input)
|
||||
throw new Error('`parse` argument must be a populated string.');
|
||||
|
||||
// Handle the "*" case
|
||||
if (input === "*") {
|
||||
return { min: undefined, max: undefined };
|
||||
}
|
||||
|
||||
var inputs = input.split(' ');
|
||||
var min;
|
||||
var max;
|
||||
|
||||
// 1.2.3 - 2.3.4
|
||||
if (inputs.length === 3 && inputs[1] === '-') {
|
||||
return { min: inputs[0], max: inputs[2] };
|
||||
}
|
||||
|
||||
inputs.forEach(function (input) {
|
||||
var parsed = parseExpression(input);
|
||||
var version = parsed.version;
|
||||
var comparator = parsed.comparator;
|
||||
|
||||
// 1.2.3
|
||||
if (inputs.length === 1 && !comparator)
|
||||
min = max = version;
|
||||
|
||||
// Parse min
|
||||
if (~comparator.indexOf('>')) {
|
||||
if (~comparator.indexOf('='))
|
||||
min = version; // >=1.2.3
|
||||
else
|
||||
min = increment(version); // >1.2.3
|
||||
}
|
||||
else if (~comparator.indexOf('<')) {
|
||||
if (~comparator.indexOf('='))
|
||||
max = version; // <=1.2.3
|
||||
else
|
||||
max = decrement(version); // <1.2.3
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
min: min,
|
||||
max : max
|
||||
};
|
||||
};
|
||||
|
||||
function parseExpression (input) {
|
||||
for (var i = 0; i < COMPARATORS.length; i++)
|
||||
if (~input.indexOf(COMPARATORS[i]))
|
||||
return {
|
||||
comparator: COMPARATORS[i],
|
||||
version: input.substr(COMPARATORS[i].length)
|
||||
};
|
||||
return { version: input, comparator: '' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a version string ('1.2.3') and returns a version string
|
||||
* that'll parse as one less than the input string ('1.2.3.-1').
|
||||
*
|
||||
* @param {String} vString
|
||||
* @return {String}
|
||||
*/
|
||||
function decrement (vString) {
|
||||
return vString + (vString.charAt(vString.length - 1) === '.' ? '' : '.') + '-1';
|
||||
}
|
||||
exports.decrement = decrement;
|
||||
|
||||
/**
|
||||
* Takes a version string ('1.2.3') and returns a version string
|
||||
* that'll parse as greater than the input string by the smallest margin
|
||||
* possible ('1.2.3.1').
|
||||
* listed as number-A, string-B, number-C, string-D in
|
||||
* Mozilla's Toolkit Format.
|
||||
* https://developer.mozilla.org/en-US/docs/Toolkit_version_format
|
||||
*
|
||||
* @param {String} vString
|
||||
* @return {String}
|
||||
*/
|
||||
function increment (vString) {
|
||||
var match = versionParse(vString);
|
||||
var a = match[1];
|
||||
var b = match[2];
|
||||
var c = match[3];
|
||||
var d = match[4];
|
||||
var lastPos = vString.length - 1;
|
||||
var lastChar = vString.charAt(lastPos);
|
||||
|
||||
if (!b) {
|
||||
return vString + (lastChar === '.' ? '' : '.') + '1';
|
||||
}
|
||||
if (!c) {
|
||||
return vString + '1';
|
||||
}
|
||||
if (!d) {
|
||||
return vString.substr(0, lastPos) + (++lastChar);
|
||||
}
|
||||
return vString.substr(0, lastPos) + String.fromCharCode(lastChar.charCodeAt(0) + 1);
|
||||
}
|
||||
exports.increment = increment;
|
||||
15
toolkit/jetpack/mozilla-toolkit-versioning/lib/utils.js
Normal file
15
toolkit/jetpack/mozilla-toolkit-versioning/lib/utils.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Breaks up a version string into the 4 components
|
||||
* defined in:
|
||||
* https://developer.mozilla.org/en-US/docs/Toolkit_version_format
|
||||
* @params {String} val
|
||||
* @return {String}
|
||||
*/
|
||||
function versionParse (val) {
|
||||
return val.match(/^([0-9\.]*)([a-zA-Z]*)([0-9\.]*)([a-zA-Z]*)$/);
|
||||
}
|
||||
exports.versionParse = versionParse;
|
||||
21
toolkit/jetpack/mozilla-toolkit-versioning/package.json
Normal file
21
toolkit/jetpack/mozilla-toolkit-versioning/package.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "mozilla-toolkit-versioning",
|
||||
"version": "0.0.2",
|
||||
"description": "Parser for Mozilla's toolkit version format",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "./node_modules/.bin/mocha --reporter spec --ui bdd"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/jsantell/mozilla-toolkit-versioning.git"
|
||||
},
|
||||
"author": "Jordan Santell",
|
||||
"license": "MIT",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"mocha": "^1.21.4",
|
||||
"chai": "^1.9.1",
|
||||
"mozilla-version-comparator": "^1.0.2"
|
||||
}
|
||||
}
|
||||
90
toolkit/jetpack/node/os.js
Normal file
90
toolkit/jetpack/node/os.js
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
/* 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';
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { Cc, Ci } = require('chrome');
|
||||
const system = require('../sdk/system');
|
||||
const runtime = require('../sdk/system/runtime');
|
||||
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
|
||||
const isWindows = system.platform === 'win32';
|
||||
const endianness = ((new Uint32Array((new Uint8Array([1,2,3,4])).buffer))[0] === 0x04030201) ? 'LE' : 'BE';
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "oscpu", () => {
|
||||
try {
|
||||
return Cc["@mozilla.org/network/protocol;1?name=http"].getService(Ci.nsIHttpProtocolHandler).oscpu;
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "hostname", () => {
|
||||
try {
|
||||
// On some platforms (Linux according to try), this service does not exist and fails.
|
||||
return Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService).myHostName;
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns a path to a temp directory
|
||||
*/
|
||||
exports.tmpdir = () => system.pathFor('TmpD');
|
||||
|
||||
/**
|
||||
* Returns the endianness of the architecture: either 'LE' or 'BE'
|
||||
*/
|
||||
exports.endianness = () => endianness;
|
||||
|
||||
/**
|
||||
* Returns hostname of the machine
|
||||
*/
|
||||
exports.hostname = () => hostname;
|
||||
|
||||
/**
|
||||
* Name of the OS type
|
||||
* Possible values:
|
||||
* https://developer.mozilla.org/en/OS_TARGET
|
||||
*/
|
||||
exports.type = () => runtime.OS;
|
||||
|
||||
/**
|
||||
* Name of the OS Platform in lower case string.
|
||||
* Possible values:
|
||||
* https://developer.mozilla.org/en/OS_TARGET
|
||||
*/
|
||||
exports.platform = () => system.platform;
|
||||
|
||||
/**
|
||||
* Type of processor architecture running:
|
||||
* 'arm', 'ia32', 'x86', 'x64'
|
||||
*/
|
||||
exports.arch = () => system.architecture;
|
||||
|
||||
/**
|
||||
* Returns the operating system release.
|
||||
*/
|
||||
exports.release = () => {
|
||||
let match = oscpu.match(/(\d[\.\d]*)/);
|
||||
return match && match.length > 1 ? match[1] : oscpu;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns EOL character for the OS
|
||||
*/
|
||||
exports.EOL = isWindows ? '\r\n' : '\n';
|
||||
|
||||
/**
|
||||
* Returns [0, 0, 0], as this is not implemented.
|
||||
*/
|
||||
exports.loadavg = () => [0, 0, 0];
|
||||
|
||||
['uptime', 'totalmem', 'freemem', 'cpus'].forEach(method => {
|
||||
exports[method] = () => { throw new Error('os.' + method + ' is not supported.'); };
|
||||
});
|
||||
182
toolkit/jetpack/sdk/addon/bootstrap.js
vendored
Normal file
182
toolkit/jetpack/sdk/addon/bootstrap.js
vendored
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
/* 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 { Cu } = require("chrome");
|
||||
const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
|
||||
const { Task: { spawn } } = require("resource://gre/modules/Task.jsm");
|
||||
const { readURI } = require("sdk/net/url");
|
||||
const { mount, unmount } = require("sdk/uri/resource");
|
||||
const { setTimeout } = require("sdk/timers");
|
||||
const { Loader, Require, Module, main, unload } = require("toolkit/loader");
|
||||
const prefs = require("sdk/preferences/service");
|
||||
|
||||
// load below now, so that it can be used by sdk/addon/runner
|
||||
// see bug https://bugzilla.mozilla.org/show_bug.cgi?id=1042239
|
||||
const Startup = Cu.import("resource://gre/modules/sdk/system/Startup.js", {});
|
||||
|
||||
const REASON = [ "unknown", "startup", "shutdown", "enable", "disable",
|
||||
"install", "uninstall", "upgrade", "downgrade" ];
|
||||
|
||||
const UUID_PATTERN = /^\{([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\}$/;
|
||||
// Takes add-on ID and normalizes it to a domain name so that add-on
|
||||
// can be mapped to resource://domain/
|
||||
const readDomain = id =>
|
||||
// If only `@` character is the first one, than just substract it,
|
||||
// otherwise fallback to legacy normalization code path. Note: `.`
|
||||
// is valid character for resource substitutaiton & we intend to
|
||||
// make add-on URIs intuitive, so it's best to just stick to an
|
||||
// add-on author typed input.
|
||||
id.lastIndexOf("@") === 0 ? id.substr(1).toLowerCase() :
|
||||
id.toLowerCase().
|
||||
replace(/@/g, "-at-").
|
||||
replace(/\./g, "-dot-").
|
||||
replace(UUID_PATTERN, "$1");
|
||||
|
||||
const readPaths = id => {
|
||||
const base = `extensions.modules.${id}.path.`;
|
||||
const domain = readDomain(id);
|
||||
return prefs.keys(base).reduce((paths, key) => {
|
||||
const value = prefs.get(key);
|
||||
const name = key.replace(base, "");
|
||||
const path = name.split(".").join("/");
|
||||
const prefix = path.length ? `${path}/` : path;
|
||||
const uri = value.endsWith("/") ? value : `${value}/`;
|
||||
const root = `extensions.modules.${domain}.commonjs.path.${name}`;
|
||||
|
||||
mount(root, uri);
|
||||
|
||||
paths[prefix] = `resource://${root}/`;
|
||||
return paths;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const Bootstrap = function(mountURI) {
|
||||
this.mountURI = mountURI;
|
||||
this.install = this.install.bind(this);
|
||||
this.uninstall = this.uninstall.bind(this);
|
||||
this.startup = this.startup.bind(this);
|
||||
this.shutdown = this.shutdown.bind(this);
|
||||
};
|
||||
Bootstrap.prototype = {
|
||||
constructor: Bootstrap,
|
||||
mount(domain, rootURI) {
|
||||
mount(domain, rootURI);
|
||||
this.domain = domain;
|
||||
},
|
||||
unmount() {
|
||||
if (this.domain) {
|
||||
unmount(this.domain);
|
||||
this.domain = null;
|
||||
}
|
||||
},
|
||||
install(addon, reason) {
|
||||
return new Promise(resolve => resolve());
|
||||
},
|
||||
uninstall(addon, reason) {
|
||||
return new Promise(resolve => {
|
||||
const {id} = addon;
|
||||
|
||||
prefs.reset(`extensions.${id}.sdk.domain`);
|
||||
prefs.reset(`extensions.${id}.sdk.version`);
|
||||
prefs.reset(`extensions.${id}.sdk.rootURI`);
|
||||
prefs.reset(`extensions.${id}.sdk.baseURI`);
|
||||
prefs.reset(`extensions.${id}.sdk.load.reason`);
|
||||
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
startup(addon, reasonCode) {
|
||||
const { id, version, resourceURI: { spec: addonURI } } = addon;
|
||||
const rootURI = this.mountURI || addonURI;
|
||||
const reason = REASON[reasonCode];
|
||||
const self = this;
|
||||
|
||||
return spawn(function*() {
|
||||
const metadata = JSON.parse(yield readURI(`${rootURI}package.json`));
|
||||
const domain = readDomain(id);
|
||||
const baseURI = `resource://${domain}/`;
|
||||
|
||||
this.mount(domain, rootURI);
|
||||
|
||||
prefs.set(`extensions.${id}.sdk.domain`, domain);
|
||||
prefs.set(`extensions.${id}.sdk.version`, version);
|
||||
prefs.set(`extensions.${id}.sdk.rootURI`, rootURI);
|
||||
prefs.set(`extensions.${id}.sdk.baseURI`, baseURI);
|
||||
prefs.set(`extensions.${id}.sdk.load.reason`, reason);
|
||||
|
||||
const command = prefs.get(`extensions.${id}.sdk.load.command`);
|
||||
|
||||
const loader = Loader({
|
||||
id,
|
||||
isNative: true,
|
||||
checkCompatibility: true,
|
||||
prefixURI: baseURI,
|
||||
rootURI: baseURI,
|
||||
name: metadata.name,
|
||||
paths: Object.assign({
|
||||
"": "resource://gre/modules/commonjs/",
|
||||
"devtools/": "resource://devtools/",
|
||||
"./": baseURI
|
||||
}, readPaths(id)),
|
||||
manifest: metadata,
|
||||
metadata: metadata,
|
||||
modules: {
|
||||
"@test/options": {},
|
||||
},
|
||||
noQuit: prefs.get(`extensions.${id}.sdk.test.no-quit`, false)
|
||||
});
|
||||
self.loader = loader;
|
||||
|
||||
const module = Module("package.json", `${baseURI}package.json`);
|
||||
const require = Require(loader, module);
|
||||
const main = command === "test" ? "sdk/test/runner" : null;
|
||||
const prefsURI = `${baseURI}defaults/preferences/prefs.js`;
|
||||
|
||||
// Init the 'sdk/webextension' module from the bootstrap addon parameter.
|
||||
require("sdk/webextension").initFromBootstrapAddonParam(addon);
|
||||
|
||||
const { startup } = require("sdk/addon/runner");
|
||||
startup(reason, {loader, main, prefsURI});
|
||||
}.bind(this)).catch(error => {
|
||||
console.error(`Failed to start ${id} addon`, error);
|
||||
throw error;
|
||||
});
|
||||
},
|
||||
shutdown(addon, code) {
|
||||
this.unmount();
|
||||
return this.unload(REASON[code]);
|
||||
},
|
||||
unload(reason) {
|
||||
return new Promise(resolve => {
|
||||
const { loader } = this;
|
||||
if (loader) {
|
||||
this.loader = null;
|
||||
unload(loader, reason);
|
||||
|
||||
setTimeout(() => {
|
||||
for (let uri of Object.keys(loader.sandboxes)) {
|
||||
let sandbox = loader.sandboxes[uri];
|
||||
if (Cu.getClassName(sandbox, true) == "Sandbox")
|
||||
Cu.nukeSandbox(sandbox);
|
||||
delete loader.sandboxes[uri];
|
||||
delete loader.modules[uri];
|
||||
}
|
||||
|
||||
try {
|
||||
Cu.nukeSandbox(loader.sharedGlobalSandbox);
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
}
|
||||
|
||||
resolve();
|
||||
}, 1000);
|
||||
}
|
||||
else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
exports.Bootstrap = Bootstrap;
|
||||
56
toolkit/jetpack/sdk/addon/events.js
Normal file
56
toolkit/jetpack/sdk/addon/events.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/* 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';
|
||||
|
||||
module.metadata = {
|
||||
'stability': 'experimental'
|
||||
};
|
||||
|
||||
var { request: hostReq, response: hostRes } = require('./host');
|
||||
var { defer: async } = require('../lang/functional');
|
||||
var { defer } = require('../core/promise');
|
||||
var { emit: emitSync, on, off } = require('../event/core');
|
||||
var { uuid } = require('../util/uuid');
|
||||
var emit = async(emitSync);
|
||||
|
||||
// Map of IDs to deferreds
|
||||
var requests = new Map();
|
||||
|
||||
// May not be necessary to wrap this in `async`
|
||||
// once promises are async via bug 881047
|
||||
var receive = async(function ({data, id, error}) {
|
||||
let request = requests.get(id);
|
||||
if (request) {
|
||||
if (error) request.reject(error);
|
||||
else request.resolve(clone(data));
|
||||
requests.delete(id);
|
||||
}
|
||||
});
|
||||
on(hostRes, 'data', receive);
|
||||
|
||||
/*
|
||||
* Send is a helper to be used in client APIs to send
|
||||
* a request to host
|
||||
*/
|
||||
function send (eventName, data) {
|
||||
let id = uuid();
|
||||
let deferred = defer();
|
||||
requests.set(id, deferred);
|
||||
emit(hostReq, 'data', {
|
||||
id: id,
|
||||
data: clone(data),
|
||||
event: eventName
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
exports.send = send;
|
||||
|
||||
/*
|
||||
* Implement internal structured cloning algorithm in the future?
|
||||
* http://www.whatwg.org/specs/web-apps/current-work/multipage/common-dom-interfaces.html#internal-structured-cloning-algorithm
|
||||
*/
|
||||
function clone (obj) {
|
||||
return JSON.parse(JSON.stringify(obj || {}));
|
||||
}
|
||||
12
toolkit/jetpack/sdk/addon/host.js
Normal file
12
toolkit/jetpack/sdk/addon/host.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
exports.request = {};
|
||||
exports.response = {};
|
||||
121
toolkit/jetpack/sdk/addon/installer.js
Normal file
121
toolkit/jetpack/sdk/addon/installer.js
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
/* 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.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const { Cc, Ci, Cu } = require("chrome");
|
||||
const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm");
|
||||
const { defer } = require("../core/promise");
|
||||
const { setTimeout } = require("../timers");
|
||||
|
||||
/**
|
||||
* `install` method error codes:
|
||||
*
|
||||
* https://developer.mozilla.org/en/Addons/Add-on_Manager/AddonManager#AddonInstall_errors
|
||||
*/
|
||||
exports.ERROR_NETWORK_FAILURE = AddonManager.ERROR_NETWORK_FAILURE;
|
||||
exports.ERROR_INCORRECT_HASH = AddonManager.ERROR_INCORRECT_HASH;
|
||||
exports.ERROR_CORRUPT_FILE = AddonManager.ERROR_CORRUPT_FILE;
|
||||
exports.ERROR_FILE_ACCESS = AddonManager.ERROR_FILE_ACCESS;
|
||||
|
||||
/**
|
||||
* Immediatly install an addon.
|
||||
*
|
||||
* @param {String} xpiPath
|
||||
* file path to an xpi file to install
|
||||
* @return {Promise}
|
||||
* A promise resolved when the addon is finally installed.
|
||||
* Resolved with addon id as value or rejected with an error code.
|
||||
*/
|
||||
exports.install = function install(xpiPath) {
|
||||
let { promise, resolve, reject } = defer();
|
||||
|
||||
// Create nsIFile for the xpi file
|
||||
let file = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsILocalFile);
|
||||
try {
|
||||
file.initWithPath(xpiPath);
|
||||
}
|
||||
catch(e) {
|
||||
reject(exports.ERROR_FILE_ACCESS);
|
||||
return promise;
|
||||
}
|
||||
|
||||
// Listen for installation end
|
||||
let listener = {
|
||||
onInstallEnded: function(aInstall, aAddon) {
|
||||
aInstall.removeListener(listener);
|
||||
// Bug 749745: on FF14+, onInstallEnded is called just before `startup()`
|
||||
// is called, but we expect to resolve the promise only after it.
|
||||
// As startup is called synchronously just after onInstallEnded,
|
||||
// a simple setTimeout(0) is enough
|
||||
setTimeout(resolve, 0, aAddon.id);
|
||||
},
|
||||
onInstallFailed: function (aInstall) {
|
||||
aInstall.removeListener(listener);
|
||||
reject(aInstall.error);
|
||||
},
|
||||
onDownloadFailed: function(aInstall) {
|
||||
this.onInstallFailed(aInstall);
|
||||
}
|
||||
};
|
||||
|
||||
// Order AddonManager to install the addon
|
||||
AddonManager.getInstallForFile(file, function(install) {
|
||||
if (install.error == 0) {
|
||||
install.addListener(listener);
|
||||
install.install();
|
||||
} else {
|
||||
reject(install.error);
|
||||
}
|
||||
});
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
exports.uninstall = function uninstall(addonId) {
|
||||
let { promise, resolve, reject } = defer();
|
||||
|
||||
// Listen for uninstallation end
|
||||
let listener = {
|
||||
onUninstalled: function onUninstalled(aAddon) {
|
||||
if (aAddon.id != addonId)
|
||||
return;
|
||||
AddonManager.removeAddonListener(listener);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
AddonManager.addAddonListener(listener);
|
||||
|
||||
// Order Addonmanager to uninstall the addon
|
||||
getAddon(addonId).then(addon => addon.uninstall(), reject);
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
exports.disable = function disable(addonId) {
|
||||
return getAddon(addonId).then(addon => {
|
||||
addon.userDisabled = true;
|
||||
return addonId;
|
||||
});
|
||||
};
|
||||
|
||||
exports.enable = function enabled(addonId) {
|
||||
return getAddon(addonId).then(addon => {
|
||||
addon.userDisabled = false;
|
||||
return addonId;
|
||||
});
|
||||
};
|
||||
|
||||
exports.isActive = function isActive(addonId) {
|
||||
return getAddon(addonId).then(addon => addon.isActive && !addon.appDisabled);
|
||||
};
|
||||
|
||||
const getAddon = function getAddon (id) {
|
||||
let { promise, resolve, reject } = defer();
|
||||
AddonManager.getAddonByID(id, addon => addon ? resolve(addon) : reject());
|
||||
return promise;
|
||||
}
|
||||
exports.getAddon = getAddon;
|
||||
18
toolkit/jetpack/sdk/addon/manager.js
Normal file
18
toolkit/jetpack/sdk/addon/manager.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const { AddonManager } = require("resource://gre/modules/AddonManager.jsm");
|
||||
const { defer } = require("../core/promise");
|
||||
|
||||
function getAddonByID(id) {
|
||||
let { promise, resolve } = defer();
|
||||
AddonManager.getAddonByID(id, resolve);
|
||||
return promise;
|
||||
}
|
||||
exports.getAddonByID = getAddonByID;
|
||||
180
toolkit/jetpack/sdk/addon/runner.js
Normal file
180
toolkit/jetpack/sdk/addon/runner.js
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
/* 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.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const { Cc, Ci, Cu } = require('chrome');
|
||||
const { rootURI, metadata, isNative } = require('@loader/options');
|
||||
const { id, loadReason } = require('../self');
|
||||
const { descriptor, Sandbox, evaluate, main, resolveURI } = require('toolkit/loader');
|
||||
const { once } = require('../system/events');
|
||||
const { exit, env, staticArgs } = require('../system');
|
||||
const { when: unload } = require('../system/unload');
|
||||
const globals = require('../system/globals');
|
||||
const xulApp = require('../system/xul-app');
|
||||
const { get } = require('../preferences/service');
|
||||
const appShellService = Cc['@mozilla.org/appshell/appShellService;1'].
|
||||
getService(Ci.nsIAppShellService);
|
||||
const { preferences } = metadata;
|
||||
|
||||
const Startup = Cu.import("resource://gre/modules/sdk/system/Startup.js", {}).exports;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
XPCOMUtils.defineLazyGetter(this, "BrowserToolboxProcess", function () {
|
||||
return Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {}).
|
||||
BrowserToolboxProcess;
|
||||
});
|
||||
|
||||
// Initializes default preferences
|
||||
function setDefaultPrefs(prefsURI) {
|
||||
const prefs = Cc['@mozilla.org/preferences-service;1'].
|
||||
getService(Ci.nsIPrefService).
|
||||
QueryInterface(Ci.nsIPrefBranch2);
|
||||
const branch = prefs.getDefaultBranch('');
|
||||
const sandbox = Sandbox({
|
||||
name: prefsURI,
|
||||
prototype: {
|
||||
pref: function(key, val) {
|
||||
switch (typeof val) {
|
||||
case 'boolean':
|
||||
branch.setBoolPref(key, val);
|
||||
break;
|
||||
case 'number':
|
||||
if (val % 1 == 0) // number must be a integer, otherwise ignore it
|
||||
branch.setIntPref(key, val);
|
||||
break;
|
||||
case 'string':
|
||||
branch.setCharPref(key, val);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// load preferences.
|
||||
evaluate(sandbox, prefsURI);
|
||||
}
|
||||
|
||||
function definePseudo(loader, id, exports) {
|
||||
let uri = resolveURI(id, loader.mapping);
|
||||
loader.modules[uri] = { exports: exports };
|
||||
}
|
||||
|
||||
function startup(reason, options) {
|
||||
return Startup.onceInitialized.then(() => {
|
||||
// Inject globals ASAP in order to have console API working ASAP
|
||||
Object.defineProperties(options.loader.globals, descriptor(globals));
|
||||
|
||||
// NOTE: Module is intentionally required only now because it relies
|
||||
// on existence of hidden window, which does not exists until startup.
|
||||
let { ready } = require('../addon/window');
|
||||
// Load localization manifest and .properties files.
|
||||
// Run the addon even in case of error (best effort approach)
|
||||
require('../l10n/loader').
|
||||
load(rootURI).
|
||||
then(null, function failure(error) {
|
||||
if (!isNative)
|
||||
console.info("Error while loading localization: " + error.message);
|
||||
}).
|
||||
then(function onLocalizationReady(data) {
|
||||
// Exports data to a pseudo module so that api-utils/l10n/core
|
||||
// can get access to it
|
||||
definePseudo(options.loader, '@l10n/data', data ? data : null);
|
||||
return ready;
|
||||
}).then(function() {
|
||||
run(options);
|
||||
}).then(null, console.exception);
|
||||
return void 0; // otherwise we raise a warning, see bug 910304
|
||||
});
|
||||
}
|
||||
|
||||
function run(options) {
|
||||
try {
|
||||
// Try initializing HTML localization before running main module. Just print
|
||||
// an exception in case of error, instead of preventing addon to be run.
|
||||
try {
|
||||
// Do not enable HTML localization while running test as it is hard to
|
||||
// disable. Because unit tests are evaluated in a another Loader who
|
||||
// doesn't have access to this current loader.
|
||||
if (options.main !== 'sdk/test/runner') {
|
||||
require('../l10n/html').enable();
|
||||
}
|
||||
}
|
||||
catch(error) {
|
||||
console.exception(error);
|
||||
}
|
||||
|
||||
// native-options does stuff directly with preferences key from package.json
|
||||
if (preferences && preferences.length > 0) {
|
||||
try {
|
||||
require('../preferences/native-options').
|
||||
enable({ preferences: preferences, id: id }).
|
||||
catch(console.exception);
|
||||
}
|
||||
catch (error) {
|
||||
console.exception(error);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// keeping support for addons packaged with older SDK versions,
|
||||
// when cfx didn't include the 'preferences' key in @loader/options
|
||||
|
||||
// Initialize inline options localization, without preventing addon to be
|
||||
// run in case of error
|
||||
try {
|
||||
require('../l10n/prefs').enable();
|
||||
}
|
||||
catch(error) {
|
||||
console.exception(error);
|
||||
}
|
||||
|
||||
// TODO: When bug 564675 is implemented this will no longer be needed
|
||||
// Always set the default prefs, because they disappear on restart
|
||||
if (options.prefsURI) {
|
||||
// Only set if `prefsURI` specified
|
||||
try {
|
||||
setDefaultPrefs(options.prefsURI);
|
||||
}
|
||||
catch (err) {
|
||||
// cfx bootstrap always passes prefsURI, even in addons without prefs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this is where the addon's main.js finally run.
|
||||
let program = main(options.loader, options.main);
|
||||
|
||||
if (typeof(program.onUnload) === 'function')
|
||||
unload(program.onUnload);
|
||||
|
||||
if (typeof(program.main) === 'function') {
|
||||
program.main({
|
||||
loadReason: loadReason,
|
||||
staticArgs: staticArgs
|
||||
}, {
|
||||
print: function print(_) { dump(_ + '\n') },
|
||||
quit: exit
|
||||
});
|
||||
}
|
||||
|
||||
if (get("extensions." + id + ".sdk.debug.show", false)) {
|
||||
BrowserToolboxProcess.init({ addonID: id });
|
||||
}
|
||||
} catch (error) {
|
||||
console.exception(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
exports.startup = startup;
|
||||
|
||||
// If add-on is lunched via `cfx run` we need to use `system.exit` to let
|
||||
// cfx know we're done (`cfx test` will take care of exit so we don't do
|
||||
// anything here).
|
||||
if (env.CFX_COMMAND === 'run') {
|
||||
unload(function(reason) {
|
||||
if (reason === 'shutdown')
|
||||
exit(0);
|
||||
});
|
||||
}
|
||||
66
toolkit/jetpack/sdk/addon/window.js
Normal file
66
toolkit/jetpack/sdk/addon/window.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const { Ci, Cc } = require("chrome");
|
||||
const { make: makeWindow, getHiddenWindow } = require("../window/utils");
|
||||
const { create: makeFrame, getDocShell } = require("../frame/utils");
|
||||
const { defer } = require("../core/promise");
|
||||
const { when: unload } = require("../system/unload");
|
||||
const cfxArgs = require("../test/options");
|
||||
|
||||
var addonPrincipal = Cc["@mozilla.org/systemprincipal;1"].
|
||||
createInstance(Ci.nsIPrincipal);
|
||||
|
||||
var hiddenWindow = getHiddenWindow();
|
||||
|
||||
if (cfxArgs.parseable) {
|
||||
console.info("hiddenWindow document.documentURI:" +
|
||||
hiddenWindow.document.documentURI);
|
||||
console.info("hiddenWindow document.readyState:" +
|
||||
hiddenWindow.document.readyState);
|
||||
}
|
||||
|
||||
// Once Bug 565388 is fixed and shipped we'll be able to make invisible,
|
||||
// permanent docShells. Meanwhile we create hidden top level window and
|
||||
// use it's docShell.
|
||||
var frame = makeFrame(hiddenWindow.document, {
|
||||
nodeName: "iframe",
|
||||
namespaceURI: "http://www.w3.org/1999/xhtml",
|
||||
allowJavascript: true,
|
||||
allowPlugins: true
|
||||
})
|
||||
var docShell = getDocShell(frame);
|
||||
var eventTarget = docShell.chromeEventHandler;
|
||||
|
||||
// We need to grant docShell system principals in order to load XUL document
|
||||
// from data URI into it.
|
||||
docShell.createAboutBlankContentViewer(addonPrincipal);
|
||||
|
||||
// Get a reference to the DOM window of the given docShell and load
|
||||
// such document into that would allow us to create XUL iframes, that
|
||||
// are necessary for hidden frames etc..
|
||||
var window = docShell.contentViewer.DOMDocument.defaultView;
|
||||
window.location = "data:application/vnd.mozilla.xul+xml;charset=utf-8,<window/>";
|
||||
|
||||
// Create a promise that is delivered once add-on window is interactive,
|
||||
// used by add-on runner to defer add-on loading until window is ready.
|
||||
var { promise, resolve } = defer();
|
||||
eventTarget.addEventListener("DOMContentLoaded", function handler(event) {
|
||||
eventTarget.removeEventListener("DOMContentLoaded", handler, false);
|
||||
resolve();
|
||||
}, false);
|
||||
|
||||
exports.ready = promise;
|
||||
exports.window = window;
|
||||
|
||||
// Still close window on unload to claim memory back early.
|
||||
unload(function() {
|
||||
window.close()
|
||||
frame.parentNode.removeChild(frame);
|
||||
});
|
||||
47
toolkit/jetpack/sdk/base64.js
Normal file
47
toolkit/jetpack/sdk/base64.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
|
||||
// Passing an empty object as second argument to avoid scope's pollution
|
||||
// (devtools loader injects these symbols as global and prevent using
|
||||
// const here)
|
||||
var { atob, btoa } = Cu.import("resource://gre/modules/Services.jsm", {});
|
||||
|
||||
function isUTF8(charset) {
|
||||
let type = typeof charset;
|
||||
|
||||
if (type === "undefined")
|
||||
return false;
|
||||
|
||||
if (type === "string" && charset.toLowerCase() === "utf-8")
|
||||
return true;
|
||||
|
||||
throw new Error("The charset argument can be only 'utf-8'");
|
||||
}
|
||||
|
||||
function toOctetChar(c) {
|
||||
return String.fromCharCode(c.charCodeAt(0) & 0xFF);
|
||||
}
|
||||
|
||||
exports.decode = function (data, charset) {
|
||||
if (isUTF8(charset))
|
||||
return decodeURIComponent(escape(atob(data)))
|
||||
|
||||
return atob(data);
|
||||
}
|
||||
|
||||
exports.encode = function (data, charset) {
|
||||
if (isUTF8(charset))
|
||||
return btoa(unescape(encodeURIComponent(data)))
|
||||
|
||||
data = data.replace(/[^\x00-\xFF]/g, toOctetChar);
|
||||
return btoa(data);
|
||||
}
|
||||
20
toolkit/jetpack/sdk/browser/events.js
Normal file
20
toolkit/jetpack/sdk/browser/events.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { events } = require("../window/events");
|
||||
const { filter } = require("../event/utils");
|
||||
const { isBrowser } = require("../window/utils");
|
||||
|
||||
// TODO: `isBrowser` detects weather window is a browser by checking
|
||||
// `windowtype` attribute, which means that all 'open' events will be
|
||||
// filtered out since document is not loaded yet. Maybe we can find a better
|
||||
// implementation for `isBrowser`. Either way it's not really needed yet
|
||||
// neither window tracker provides this event.
|
||||
|
||||
exports.events = filter(events, ({target}) => isBrowser(target));
|
||||
337
toolkit/jetpack/sdk/clipboard.js
Normal file
337
toolkit/jetpack/sdk/clipboard.js
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "stable",
|
||||
"engines": {
|
||||
// TODO Fennec Support 789757
|
||||
"Firefox": "*",
|
||||
"SeaMonkey": "*",
|
||||
"Thunderbird": "*"
|
||||
}
|
||||
};
|
||||
|
||||
const { Cc, Ci } = require("chrome");
|
||||
const { DataURL } = require("./url");
|
||||
const apiUtils = require("./deprecated/api-utils");
|
||||
/*
|
||||
While these data flavors resemble Internet media types, they do
|
||||
no directly map to them.
|
||||
*/
|
||||
const kAllowableFlavors = [
|
||||
"text/unicode",
|
||||
"text/html",
|
||||
"image/png"
|
||||
/* CURRENTLY UNSUPPORTED FLAVORS
|
||||
"text/plain",
|
||||
"image/jpg",
|
||||
"image/jpeg",
|
||||
"image/gif",
|
||||
"text/x-moz-text-internal",
|
||||
"AOLMAIL",
|
||||
"application/x-moz-file",
|
||||
"text/x-moz-url",
|
||||
"text/x-moz-url-data",
|
||||
"text/x-moz-url-desc",
|
||||
"text/x-moz-url-priv",
|
||||
"application/x-moz-nativeimage",
|
||||
"application/x-moz-nativehtml",
|
||||
"application/x-moz-file-promise-url",
|
||||
"application/x-moz-file-promise-dest-filename",
|
||||
"application/x-moz-file-promise",
|
||||
"application/x-moz-file-promise-dir"
|
||||
*/
|
||||
];
|
||||
|
||||
/*
|
||||
Aliases for common flavors. Not all flavors will
|
||||
get an alias. New aliases must be approved by a
|
||||
Jetpack API druid.
|
||||
*/
|
||||
const kFlavorMap = [
|
||||
{ short: "text", long: "text/unicode" },
|
||||
{ short: "html", long: "text/html" },
|
||||
{ short: "image", long: "image/png" }
|
||||
];
|
||||
|
||||
var clipboardService = Cc["@mozilla.org/widget/clipboard;1"].
|
||||
getService(Ci.nsIClipboard);
|
||||
|
||||
var clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].
|
||||
getService(Ci.nsIClipboardHelper);
|
||||
|
||||
var imageTools = Cc["@mozilla.org/image/tools;1"].
|
||||
getService(Ci.imgITools);
|
||||
|
||||
exports.set = function(aData, aDataType) {
|
||||
|
||||
let options = {
|
||||
data: aData,
|
||||
datatype: aDataType || "text"
|
||||
};
|
||||
|
||||
// If `aDataType` is not given or if it's "image", the data is parsed as
|
||||
// data URL to detect a better datatype
|
||||
if (aData && (!aDataType || aDataType === "image")) {
|
||||
try {
|
||||
let dataURL = new DataURL(aData);
|
||||
|
||||
options.datatype = dataURL.mimeType;
|
||||
options.data = dataURL.data;
|
||||
}
|
||||
catch (e) {
|
||||
// Ignore invalid URIs
|
||||
if (e.name !== "URIError") {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
options = apiUtils.validateOptions(options, {
|
||||
data: {
|
||||
is: ["string"]
|
||||
},
|
||||
datatype: {
|
||||
is: ["string"]
|
||||
}
|
||||
});
|
||||
|
||||
let flavor = fromJetpackFlavor(options.datatype);
|
||||
|
||||
if (!flavor)
|
||||
throw new Error("Invalid flavor for " + options.datatype);
|
||||
|
||||
// Additional checks for using the simple case
|
||||
if (flavor == "text/unicode") {
|
||||
clipboardHelper.copyString(options.data);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Below are the more complex cases where we actually have to work with a
|
||||
// nsITransferable object
|
||||
var xferable = Cc["@mozilla.org/widget/transferable;1"].
|
||||
createInstance(Ci.nsITransferable);
|
||||
if (!xferable)
|
||||
throw new Error("Couldn't set the clipboard due to an internal error " +
|
||||
"(couldn't create a Transferable object).");
|
||||
// Bug 769440: Starting with FF16, transferable have to be inited
|
||||
if ("init" in xferable)
|
||||
xferable.init(null);
|
||||
|
||||
switch (flavor) {
|
||||
case "text/html":
|
||||
// add text/html flavor
|
||||
let str = Cc["@mozilla.org/supports-string;1"].
|
||||
createInstance(Ci.nsISupportsString);
|
||||
|
||||
str.data = options.data;
|
||||
xferable.addDataFlavor(flavor);
|
||||
xferable.setTransferData(flavor, str, str.data.length * 2);
|
||||
|
||||
// add a text/unicode flavor (html converted to plain text)
|
||||
str = Cc["@mozilla.org/supports-string;1"].
|
||||
createInstance(Ci.nsISupportsString);
|
||||
let converter = Cc["@mozilla.org/feed-textconstruct;1"].
|
||||
createInstance(Ci.nsIFeedTextConstruct);
|
||||
|
||||
converter.type = "html";
|
||||
converter.text = options.data;
|
||||
str.data = converter.plainText();
|
||||
xferable.addDataFlavor("text/unicode");
|
||||
xferable.setTransferData("text/unicode", str, str.data.length * 2);
|
||||
break;
|
||||
|
||||
// Set images to the clipboard is not straightforward, to have an idea how
|
||||
// it works on platform side, see:
|
||||
// http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsCopySupport.cpp?rev=7857c5bff017#530
|
||||
case "image/png":
|
||||
let image = options.data;
|
||||
|
||||
let container = {};
|
||||
|
||||
try {
|
||||
let input = Cc["@mozilla.org/io/string-input-stream;1"].
|
||||
createInstance(Ci.nsIStringInputStream);
|
||||
|
||||
input.setData(image, image.length);
|
||||
|
||||
imageTools.decodeImageData(input, flavor, container);
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error("Unable to decode data given in a valid image.");
|
||||
}
|
||||
|
||||
// Store directly the input stream makes the cliboard's data available
|
||||
// for Firefox but not to the others application or to the OS. Therefore,
|
||||
// a `nsISupportsInterfacePointer` object that reference an `imgIContainer`
|
||||
// with the image is needed.
|
||||
var imgPtr = Cc["@mozilla.org/supports-interface-pointer;1"].
|
||||
createInstance(Ci.nsISupportsInterfacePointer);
|
||||
|
||||
imgPtr.data = container.value;
|
||||
|
||||
xferable.addDataFlavor(flavor);
|
||||
xferable.setTransferData(flavor, imgPtr, -1);
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unable to handle the flavor " + flavor + ".");
|
||||
}
|
||||
|
||||
// TODO: Not sure if this will ever actually throw. -zpao
|
||||
try {
|
||||
clipboardService.setData(
|
||||
xferable,
|
||||
null,
|
||||
clipboardService.kGlobalClipboard
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error("Couldn't set clipboard data due to an internal error: " + e);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
exports.get = function(aDataType) {
|
||||
let options = {
|
||||
datatype: aDataType
|
||||
};
|
||||
|
||||
// Figure out the best data type for the clipboard's data, if omitted
|
||||
if (!aDataType) {
|
||||
if (~currentFlavors().indexOf("image"))
|
||||
options.datatype = "image";
|
||||
else
|
||||
options.datatype = "text";
|
||||
}
|
||||
|
||||
options = apiUtils.validateOptions(options, {
|
||||
datatype: {
|
||||
is: ["string"]
|
||||
}
|
||||
});
|
||||
|
||||
var xferable = Cc["@mozilla.org/widget/transferable;1"].
|
||||
createInstance(Ci.nsITransferable);
|
||||
if (!xferable)
|
||||
throw new Error("Couldn't set the clipboard due to an internal error " +
|
||||
"(couldn't create a Transferable object).");
|
||||
// Bug 769440: Starting with FF16, transferable have to be inited
|
||||
if ("init" in xferable)
|
||||
xferable.init(null);
|
||||
|
||||
var flavor = fromJetpackFlavor(options.datatype);
|
||||
|
||||
// Ensure that the user hasn't requested a flavor that we don't support.
|
||||
if (!flavor)
|
||||
throw new Error("Getting the clipboard with the flavor '" + flavor +
|
||||
"' is not supported.");
|
||||
|
||||
// TODO: Check for matching flavor first? Probably not worth it.
|
||||
|
||||
xferable.addDataFlavor(flavor);
|
||||
// Get the data into our transferable.
|
||||
clipboardService.getData(
|
||||
xferable,
|
||||
clipboardService.kGlobalClipboard
|
||||
);
|
||||
|
||||
var data = {};
|
||||
var dataLen = {};
|
||||
try {
|
||||
xferable.getTransferData(flavor, data, dataLen);
|
||||
} catch (e) {
|
||||
// Clipboard doesn't contain data in flavor, return null.
|
||||
return null;
|
||||
}
|
||||
|
||||
// There's no data available, return.
|
||||
if (data.value === null)
|
||||
return null;
|
||||
|
||||
// TODO: Add flavors here as we support more in kAllowableFlavors.
|
||||
switch (flavor) {
|
||||
case "text/unicode":
|
||||
case "text/html":
|
||||
data = data.value.QueryInterface(Ci.nsISupportsString).data;
|
||||
break;
|
||||
case "image/png":
|
||||
let dataURL = new DataURL();
|
||||
|
||||
dataURL.mimeType = flavor;
|
||||
dataURL.base64 = true;
|
||||
|
||||
let image = data.value;
|
||||
|
||||
// Due to the differences in how images could be stored in the clipboard
|
||||
// the checks below are needed. The clipboard could already provide the
|
||||
// image as byte streams, but also as pointer, or as image container.
|
||||
// If it's not possible obtain a byte stream, the function returns `null`.
|
||||
if (image instanceof Ci.nsISupportsInterfacePointer)
|
||||
image = image.data;
|
||||
|
||||
if (image instanceof Ci.imgIContainer)
|
||||
image = imageTools.encodeImage(image, flavor);
|
||||
|
||||
if (image instanceof Ci.nsIInputStream) {
|
||||
let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].
|
||||
createInstance(Ci.nsIBinaryInputStream);
|
||||
|
||||
binaryStream.setInputStream(image);
|
||||
|
||||
dataURL.data = binaryStream.readBytes(binaryStream.available());
|
||||
|
||||
data = dataURL.toString();
|
||||
}
|
||||
else
|
||||
data = null;
|
||||
|
||||
break;
|
||||
default:
|
||||
data = null;
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
function currentFlavors() {
|
||||
// Loop over kAllowableFlavors, calling hasDataMatchingFlavors for each.
|
||||
// This doesn't seem like the most efficient way, but we can't get
|
||||
// confirmation for specific flavors any other way. This is supposed to be
|
||||
// an inexpensive call, so performance shouldn't be impacted (much).
|
||||
var currentFlavors = [];
|
||||
for (var flavor of kAllowableFlavors) {
|
||||
var matches = clipboardService.hasDataMatchingFlavors(
|
||||
[flavor],
|
||||
1,
|
||||
clipboardService.kGlobalClipboard
|
||||
);
|
||||
if (matches)
|
||||
currentFlavors.push(toJetpackFlavor(flavor));
|
||||
}
|
||||
return currentFlavors;
|
||||
};
|
||||
|
||||
Object.defineProperty(exports, "currentFlavors", { get : currentFlavors });
|
||||
|
||||
// SUPPORT FUNCTIONS ////////////////////////////////////////////////////////
|
||||
|
||||
function toJetpackFlavor(aFlavor) {
|
||||
for (let flavorMap of kFlavorMap)
|
||||
if (flavorMap.long == aFlavor)
|
||||
return flavorMap.short;
|
||||
// Return null in the case where we don't match
|
||||
return null;
|
||||
}
|
||||
|
||||
function fromJetpackFlavor(aJetpackFlavor) {
|
||||
// TODO: Handle proper flavors better
|
||||
for (let flavorMap of kFlavorMap)
|
||||
if (flavorMap.short == aJetpackFlavor || flavorMap.long == aJetpackFlavor)
|
||||
return flavorMap.long;
|
||||
// Return null in the case where we don't match.
|
||||
return null;
|
||||
}
|
||||
78
toolkit/jetpack/sdk/console/plain-text.js
Normal file
78
toolkit/jetpack/sdk/console/plain-text.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { Cc, Ci, Cu, Cr } = require("chrome");
|
||||
const self = require("../self");
|
||||
const prefs = require("../preferences/service");
|
||||
const { merge } = require("../util/object");
|
||||
const { ConsoleAPI } = Cu.import("resource://gre/modules/Console.jsm", {});
|
||||
|
||||
const DEFAULT_LOG_LEVEL = "error";
|
||||
const ADDON_LOG_LEVEL_PREF = "extensions." + self.id + ".sdk.console.logLevel";
|
||||
const SDK_LOG_LEVEL_PREF = "extensions.sdk.console.logLevel";
|
||||
|
||||
var logLevel = DEFAULT_LOG_LEVEL;
|
||||
function setLogLevel() {
|
||||
logLevel = prefs.get(ADDON_LOG_LEVEL_PREF,
|
||||
prefs.get(SDK_LOG_LEVEL_PREF,
|
||||
DEFAULT_LOG_LEVEL));
|
||||
}
|
||||
setLogLevel();
|
||||
|
||||
var logLevelObserver = {
|
||||
QueryInterface: function(iid) {
|
||||
if (!iid.equals(Ci.nsIObserver) &&
|
||||
!iid.equals(Ci.nsISupportsWeakReference) &&
|
||||
!iid.equals(Ci.nsISupports))
|
||||
throw Cr.NS_ERROR_NO_INTERFACE;
|
||||
return this;
|
||||
},
|
||||
observe: function(subject, topic, data) {
|
||||
setLogLevel();
|
||||
}
|
||||
};
|
||||
var branch = Cc["@mozilla.org/preferences-service;1"].
|
||||
getService(Ci.nsIPrefService).
|
||||
getBranch(null);
|
||||
branch.addObserver(ADDON_LOG_LEVEL_PREF, logLevelObserver, true);
|
||||
branch.addObserver(SDK_LOG_LEVEL_PREF, logLevelObserver, true);
|
||||
|
||||
function PlainTextConsole(print, innerID) {
|
||||
|
||||
let consoleOptions = {
|
||||
prefix: self.name,
|
||||
maxLogLevel: logLevel,
|
||||
dump: print,
|
||||
innerID: innerID,
|
||||
consoleID: "addon/" + self.id
|
||||
};
|
||||
let console = new ConsoleAPI(consoleOptions);
|
||||
|
||||
// As we freeze the console object, we can't modify this property afterward
|
||||
Object.defineProperty(console, "maxLogLevel", {
|
||||
get: function() {
|
||||
return logLevel;
|
||||
}
|
||||
});
|
||||
|
||||
// We defined the `__exposedProps__` in our console chrome object.
|
||||
//
|
||||
// Meanwhile we're investigating with the platform team if `__exposedProps__`
|
||||
// are needed, or are just a left-over.
|
||||
|
||||
console.__exposedProps__ = Object.keys(ConsoleAPI.prototype).reduce(function(exposed, prop) {
|
||||
exposed[prop] = "r";
|
||||
return exposed;
|
||||
}, {});
|
||||
|
||||
Object.freeze(console);
|
||||
return console;
|
||||
};
|
||||
exports.PlainTextConsole = PlainTextConsole;
|
||||
86
toolkit/jetpack/sdk/console/traceback.js
Normal file
86
toolkit/jetpack/sdk/console/traceback.js
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const { Ci, components } = require("chrome");
|
||||
const { parseStack, sourceURI } = require("toolkit/loader");
|
||||
const { readURISync } = require("../net/url");
|
||||
|
||||
function safeGetFileLine(path, line) {
|
||||
try {
|
||||
var scheme = require("../url").URL(path).scheme;
|
||||
// TODO: There should be an easier, more accurate way to figure out
|
||||
// what's the case here.
|
||||
if (!(scheme == "http" || scheme == "https"))
|
||||
return readURISync(path).split("\n")[line - 1];
|
||||
} catch (e) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
function nsIStackFramesToJSON(frame) {
|
||||
var stack = [];
|
||||
|
||||
while (frame) {
|
||||
if (frame.filename) {
|
||||
stack.unshift({
|
||||
fileName: sourceURI(frame.filename),
|
||||
lineNumber: frame.lineNumber,
|
||||
name: frame.name
|
||||
});
|
||||
}
|
||||
frame = frame.caller;
|
||||
}
|
||||
|
||||
return stack;
|
||||
};
|
||||
|
||||
var fromException = exports.fromException = function fromException(e) {
|
||||
if (e instanceof Ci.nsIException)
|
||||
return nsIStackFramesToJSON(e.location);
|
||||
if (e.stack && e.stack.length)
|
||||
return parseStack(e.stack);
|
||||
if (e.fileName && typeof(e.lineNumber == "number"))
|
||||
return [{fileName: sourceURI(e.fileName),
|
||||
lineNumber: e.lineNumber,
|
||||
name: null}];
|
||||
return [];
|
||||
};
|
||||
|
||||
var get = exports.get = function get() {
|
||||
return nsIStackFramesToJSON(components.stack.caller);
|
||||
};
|
||||
|
||||
var format = exports.format = function format(tbOrException) {
|
||||
if (tbOrException === undefined) {
|
||||
tbOrException = get();
|
||||
tbOrException.pop();
|
||||
}
|
||||
|
||||
var tb;
|
||||
if (typeof(tbOrException) == "object" &&
|
||||
tbOrException.constructor.name == "Array")
|
||||
tb = tbOrException;
|
||||
else
|
||||
tb = fromException(tbOrException);
|
||||
|
||||
var lines = ["Traceback (most recent call last):"];
|
||||
|
||||
tb.forEach(
|
||||
function(frame) {
|
||||
if (!(frame.fileName || frame.lineNumber || frame.name))
|
||||
return;
|
||||
|
||||
lines.push(' File "' + frame.fileName + '", line ' +
|
||||
frame.lineNumber + ', in ' + frame.name);
|
||||
var sourceLine = safeGetFileLine(frame.fileName, frame.lineNumber);
|
||||
if (sourceLine)
|
||||
lines.push(' ' + sourceLine.trim());
|
||||
});
|
||||
|
||||
return lines.join("\n");
|
||||
};
|
||||
305
toolkit/jetpack/sdk/content/content-worker.js
Normal file
305
toolkit/jetpack/sdk/content/content-worker.js
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
/* 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/. */
|
||||
|
||||
Object.freeze({
|
||||
// TODO: Bug 727854 Use same implementation than common JS modules,
|
||||
// i.e. EventEmitter module
|
||||
|
||||
/**
|
||||
* Create an EventEmitter instance.
|
||||
*/
|
||||
createEventEmitter: function createEventEmitter(emit) {
|
||||
let listeners = Object.create(null);
|
||||
let eventEmitter = Object.freeze({
|
||||
emit: emit,
|
||||
on: function on(name, callback) {
|
||||
if (typeof callback !== "function")
|
||||
return this;
|
||||
if (!(name in listeners))
|
||||
listeners[name] = [];
|
||||
listeners[name].push(callback);
|
||||
return this;
|
||||
},
|
||||
once: function once(name, callback) {
|
||||
eventEmitter.on(name, function onceCallback() {
|
||||
eventEmitter.removeListener(name, onceCallback);
|
||||
callback.apply(callback, arguments);
|
||||
});
|
||||
},
|
||||
removeListener: function removeListener(name, callback) {
|
||||
if (!(name in listeners))
|
||||
return;
|
||||
let index = listeners[name].indexOf(callback);
|
||||
if (index == -1)
|
||||
return;
|
||||
listeners[name].splice(index, 1);
|
||||
}
|
||||
});
|
||||
function onEvent(name) {
|
||||
if (!(name in listeners))
|
||||
return [];
|
||||
let args = Array.slice(arguments, 1);
|
||||
let results = [];
|
||||
for (let callback of listeners[name]) {
|
||||
results.push(callback.apply(null, args));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
return {
|
||||
eventEmitter: eventEmitter,
|
||||
emit: onEvent
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Create an EventEmitter instance to communicate with chrome module
|
||||
* by passing only strings between compartments.
|
||||
* This function expects `emitToChrome` function, that allows to send
|
||||
* events to the chrome module. It returns the EventEmitter as `pipe`
|
||||
* attribute, and, `onChromeEvent` a function that allows chrome module
|
||||
* to send event into the EventEmitter.
|
||||
*
|
||||
* pipe.emit --> emitToChrome
|
||||
* onChromeEvent --> callback registered through pipe.on
|
||||
*/
|
||||
createPipe: function createPipe(emitToChrome) {
|
||||
let ContentWorker = this;
|
||||
function onEvent(type, ...args) {
|
||||
// JSON.stringify is buggy with cross-sandbox values,
|
||||
// it may return "{}" on functions. Use a replacer to match them correctly.
|
||||
let replacer = (k, v) =>
|
||||
typeof(v) === "function"
|
||||
? (type === "console" ? Function.toString.call(v) : void(0))
|
||||
: v;
|
||||
|
||||
let str = JSON.stringify([type, ...args], replacer);
|
||||
emitToChrome(str);
|
||||
}
|
||||
|
||||
let { eventEmitter, emit } =
|
||||
ContentWorker.createEventEmitter(onEvent);
|
||||
|
||||
return {
|
||||
pipe: eventEmitter,
|
||||
onChromeEvent: function onChromeEvent(array) {
|
||||
// We either receive a stringified array, or a real array.
|
||||
// We still allow to pass an array of objects, in WorkerSandbox.emitSync
|
||||
// in order to allow sending DOM node reference between content script
|
||||
// and modules (only used for context-menu API)
|
||||
let args = typeof array == "string" ? JSON.parse(array) : array;
|
||||
return emit.apply(null, args);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
injectConsole: function injectConsole(exports, pipe) {
|
||||
exports.console = Object.freeze({
|
||||
log: pipe.emit.bind(null, "console", "log"),
|
||||
info: pipe.emit.bind(null, "console", "info"),
|
||||
warn: pipe.emit.bind(null, "console", "warn"),
|
||||
error: pipe.emit.bind(null, "console", "error"),
|
||||
debug: pipe.emit.bind(null, "console", "debug"),
|
||||
exception: pipe.emit.bind(null, "console", "exception"),
|
||||
trace: pipe.emit.bind(null, "console", "trace"),
|
||||
time: pipe.emit.bind(null, "console", "time"),
|
||||
timeEnd: pipe.emit.bind(null, "console", "timeEnd")
|
||||
});
|
||||
},
|
||||
|
||||
injectTimers: function injectTimers(exports, chromeAPI, pipe, console) {
|
||||
// wrapped functions from `'timer'` module.
|
||||
// Wrapper adds `try catch` blocks to the callbacks in order to
|
||||
// emit `error` event if exception is thrown in
|
||||
// the Worker global scope.
|
||||
// @see http://www.w3.org/TR/workers/#workerutils
|
||||
|
||||
// List of all living timeouts/intervals
|
||||
let _timers = Object.create(null);
|
||||
|
||||
// Keep a reference to original timeout functions
|
||||
let {
|
||||
setTimeout: chromeSetTimeout,
|
||||
setInterval: chromeSetInterval,
|
||||
clearTimeout: chromeClearTimeout,
|
||||
clearInterval: chromeClearInterval
|
||||
} = chromeAPI.timers;
|
||||
|
||||
function registerTimer(timer) {
|
||||
let registerMethod = null;
|
||||
if (timer.kind == "timeout")
|
||||
registerMethod = chromeSetTimeout;
|
||||
else if (timer.kind == "interval")
|
||||
registerMethod = chromeSetInterval;
|
||||
else
|
||||
throw new Error("Unknown timer kind: " + timer.kind);
|
||||
|
||||
if (typeof timer.fun == 'string') {
|
||||
let code = timer.fun;
|
||||
timer.fun = () => chromeAPI.sandbox.evaluate(exports, code);
|
||||
} else if (typeof timer.fun != 'function') {
|
||||
throw new Error('Unsupported callback type' + typeof timer.fun);
|
||||
}
|
||||
|
||||
let id = registerMethod(onFire, timer.delay);
|
||||
function onFire() {
|
||||
try {
|
||||
if (timer.kind == "timeout")
|
||||
delete _timers[id];
|
||||
timer.fun.apply(null, timer.args);
|
||||
} catch(e) {
|
||||
console.exception(e);
|
||||
let wrapper = {
|
||||
instanceOfError: instanceOf(e, Error),
|
||||
value: e,
|
||||
};
|
||||
if (wrapper.instanceOfError) {
|
||||
wrapper.value = {
|
||||
message: e.message,
|
||||
fileName: e.fileName,
|
||||
lineNumber: e.lineNumber,
|
||||
stack: e.stack,
|
||||
name: e.name,
|
||||
};
|
||||
}
|
||||
pipe.emit('error', wrapper);
|
||||
}
|
||||
}
|
||||
_timers[id] = timer;
|
||||
return id;
|
||||
}
|
||||
|
||||
// copied from sdk/lang/type.js since modules are not available here
|
||||
function instanceOf(value, Type) {
|
||||
var isConstructorNameSame;
|
||||
var isConstructorSourceSame;
|
||||
|
||||
// If `instanceof` returned `true` we know result right away.
|
||||
var isInstanceOf = value instanceof Type;
|
||||
|
||||
// If `instanceof` returned `false` we do ducktype check since `Type` may be
|
||||
// from a different sandbox. If a constructor of the `value` or a constructor
|
||||
// of the value's prototype has same name and source we assume that it's an
|
||||
// instance of the Type.
|
||||
if (!isInstanceOf && value) {
|
||||
isConstructorNameSame = value.constructor.name === Type.name;
|
||||
isConstructorSourceSame = String(value.constructor) == String(Type);
|
||||
isInstanceOf = (isConstructorNameSame && isConstructorSourceSame) ||
|
||||
instanceOf(Object.getPrototypeOf(value), Type);
|
||||
}
|
||||
return isInstanceOf;
|
||||
}
|
||||
|
||||
function unregisterTimer(id) {
|
||||
if (!(id in _timers))
|
||||
return;
|
||||
let { kind } = _timers[id];
|
||||
delete _timers[id];
|
||||
if (kind == "timeout")
|
||||
chromeClearTimeout(id);
|
||||
else if (kind == "interval")
|
||||
chromeClearInterval(id);
|
||||
else
|
||||
throw new Error("Unknown timer kind: " + kind);
|
||||
}
|
||||
|
||||
function disableAllTimers() {
|
||||
Object.keys(_timers).forEach(unregisterTimer);
|
||||
}
|
||||
|
||||
exports.setTimeout = function ContentScriptSetTimeout(callback, delay) {
|
||||
return registerTimer({
|
||||
kind: "timeout",
|
||||
fun: callback,
|
||||
delay: delay,
|
||||
args: Array.slice(arguments, 2)
|
||||
});
|
||||
};
|
||||
exports.clearTimeout = function ContentScriptClearTimeout(id) {
|
||||
unregisterTimer(id);
|
||||
};
|
||||
|
||||
exports.setInterval = function ContentScriptSetInterval(callback, delay) {
|
||||
return registerTimer({
|
||||
kind: "interval",
|
||||
fun: callback,
|
||||
delay: delay,
|
||||
args: Array.slice(arguments, 2)
|
||||
});
|
||||
};
|
||||
exports.clearInterval = function ContentScriptClearInterval(id) {
|
||||
unregisterTimer(id);
|
||||
};
|
||||
|
||||
// On page-hide, save a list of all existing timers before disabling them,
|
||||
// in order to be able to restore them on page-show.
|
||||
// These events are fired when the page goes in/out of bfcache.
|
||||
// https://developer.mozilla.org/En/Working_with_BFCache
|
||||
let frozenTimers = [];
|
||||
pipe.on("pageshow", function onPageShow() {
|
||||
frozenTimers.forEach(registerTimer);
|
||||
});
|
||||
pipe.on("pagehide", function onPageHide() {
|
||||
frozenTimers = [];
|
||||
for (let id in _timers)
|
||||
frozenTimers.push(_timers[id]);
|
||||
disableAllTimers();
|
||||
// Some other pagehide listeners may register some timers that won't be
|
||||
// frozen as this particular pagehide listener is called first.
|
||||
// So freeze these timers on next cycle.
|
||||
chromeSetTimeout(function () {
|
||||
for (let id in _timers)
|
||||
frozenTimers.push(_timers[id]);
|
||||
disableAllTimers();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Unregister all timers when the page is destroyed
|
||||
// (i.e. when it is removed from bfcache)
|
||||
pipe.on("detach", function clearTimeouts() {
|
||||
disableAllTimers();
|
||||
_timers = {};
|
||||
frozenTimers = [];
|
||||
});
|
||||
},
|
||||
|
||||
injectMessageAPI: function injectMessageAPI(exports, pipe, console) {
|
||||
|
||||
let ContentWorker = this;
|
||||
let { eventEmitter: port, emit : portEmit } =
|
||||
ContentWorker.createEventEmitter(pipe.emit.bind(null, "event"));
|
||||
pipe.on("event", portEmit);
|
||||
|
||||
let self = {
|
||||
port: port,
|
||||
postMessage: pipe.emit.bind(null, "message"),
|
||||
on: pipe.on.bind(null),
|
||||
once: pipe.once.bind(null),
|
||||
removeListener: pipe.removeListener.bind(null),
|
||||
};
|
||||
Object.defineProperty(exports, "self", {
|
||||
value: self
|
||||
});
|
||||
},
|
||||
|
||||
injectOptions: function (exports, options) {
|
||||
Object.defineProperty( exports.self, "options", { value: JSON.parse( options ) });
|
||||
},
|
||||
|
||||
inject: function (exports, chromeAPI, emitToChrome, options) {
|
||||
let ContentWorker = this;
|
||||
let { pipe, onChromeEvent } =
|
||||
ContentWorker.createPipe(emitToChrome);
|
||||
|
||||
ContentWorker.injectConsole(exports, pipe);
|
||||
ContentWorker.injectTimers(exports, chromeAPI, pipe, exports.console);
|
||||
ContentWorker.injectMessageAPI(exports, pipe, exports.console);
|
||||
if ( options !== undefined ) {
|
||||
ContentWorker.injectOptions(exports, options);
|
||||
}
|
||||
|
||||
Object.freeze( exports.self );
|
||||
|
||||
return onChromeEvent;
|
||||
}
|
||||
});
|
||||
17
toolkit/jetpack/sdk/content/content.js
Normal file
17
toolkit/jetpack/sdk/content/content.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "deprecated"
|
||||
};
|
||||
|
||||
const { deprecateUsage } = require('../util/deprecate');
|
||||
|
||||
Object.defineProperty(exports, "Worker", {
|
||||
get: function() {
|
||||
deprecateUsage('`sdk/content/content` is deprecated. Please use `sdk/content/worker` directly.');
|
||||
return require('./worker').Worker;
|
||||
}
|
||||
});
|
||||
408
toolkit/jetpack/sdk/content/context-menu.js
Normal file
408
toolkit/jetpack/sdk/content/context-menu.js
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
/* 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 { Class } = require("../core/heritage");
|
||||
const self = require("../self");
|
||||
const { WorkerChild } = require("./worker-child");
|
||||
const { getInnerId } = require("../window/utils");
|
||||
const { Ci } = require("chrome");
|
||||
const { Services } = require("resource://gre/modules/Services.jsm");
|
||||
const system = require('../system/events');
|
||||
const { process } = require('../remote/child');
|
||||
|
||||
// These functions are roughly copied from sdk/selection which doesn't work
|
||||
// in the content process
|
||||
function getElementWithSelection(window) {
|
||||
let element = Services.focus.getFocusedElementForWindow(window, false, {});
|
||||
if (!element)
|
||||
return null;
|
||||
|
||||
try {
|
||||
// Accessing selectionStart and selectionEnd on e.g. a button
|
||||
// results in an exception thrown as per the HTML5 spec. See
|
||||
// http://www.whatwg.org/specs/web-apps/current-work/multipage/association-of-controls-and-forms.html#textFieldSelection
|
||||
|
||||
let { value, selectionStart, selectionEnd } = element;
|
||||
|
||||
let hasSelection = typeof value === "string" &&
|
||||
!isNaN(selectionStart) &&
|
||||
!isNaN(selectionEnd) &&
|
||||
selectionStart !== selectionEnd;
|
||||
|
||||
return hasSelection ? element : null;
|
||||
}
|
||||
catch (err) {
|
||||
console.exception(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function safeGetRange(selection, rangeNumber) {
|
||||
try {
|
||||
let { rangeCount } = selection;
|
||||
let range = null;
|
||||
|
||||
for (let rangeNumber = 0; rangeNumber < rangeCount; rangeNumber++ ) {
|
||||
range = selection.getRangeAt(rangeNumber);
|
||||
|
||||
if (range && range.toString())
|
||||
break;
|
||||
|
||||
range = null;
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getSelection(window) {
|
||||
let selection = window.getSelection();
|
||||
let range = safeGetRange(selection);
|
||||
if (range)
|
||||
return range.toString();
|
||||
|
||||
let node = getElementWithSelection(window);
|
||||
if (!node)
|
||||
return null;
|
||||
|
||||
return node.value.substring(node.selectionStart, node.selectionEnd);
|
||||
}
|
||||
|
||||
//These are used by PageContext.isCurrent below. If the popupNode or any of
|
||||
//its ancestors is one of these, Firefox uses a tailored context menu, and so
|
||||
//the page context doesn't apply.
|
||||
const NON_PAGE_CONTEXT_ELTS = [
|
||||
Ci.nsIDOMHTMLAnchorElement,
|
||||
Ci.nsIDOMHTMLAppletElement,
|
||||
Ci.nsIDOMHTMLAreaElement,
|
||||
Ci.nsIDOMHTMLButtonElement,
|
||||
Ci.nsIDOMHTMLCanvasElement,
|
||||
Ci.nsIDOMHTMLEmbedElement,
|
||||
Ci.nsIDOMHTMLImageElement,
|
||||
Ci.nsIDOMHTMLInputElement,
|
||||
Ci.nsIDOMHTMLMapElement,
|
||||
Ci.nsIDOMHTMLMediaElement,
|
||||
Ci.nsIDOMHTMLMenuElement,
|
||||
Ci.nsIDOMHTMLObjectElement,
|
||||
Ci.nsIDOMHTMLOptionElement,
|
||||
Ci.nsIDOMHTMLSelectElement,
|
||||
Ci.nsIDOMHTMLTextAreaElement,
|
||||
];
|
||||
|
||||
// List all editable types of inputs. Or is it better to have a list
|
||||
// of non-editable inputs?
|
||||
var editableInputs = {
|
||||
email: true,
|
||||
number: true,
|
||||
password: true,
|
||||
search: true,
|
||||
tel: true,
|
||||
text: true,
|
||||
textarea: true,
|
||||
url: true
|
||||
};
|
||||
|
||||
var CONTEXTS = {};
|
||||
|
||||
var Context = Class({
|
||||
initialize: function(id) {
|
||||
this.id = id;
|
||||
},
|
||||
|
||||
adjustPopupNode: function adjustPopupNode(popupNode) {
|
||||
return popupNode;
|
||||
},
|
||||
|
||||
// Gets state to pass through to the parent process for the node the user
|
||||
// clicked on
|
||||
getState: function(popupNode) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Matches when the context-clicked node doesn't have any of
|
||||
// NON_PAGE_CONTEXT_ELTS in its ancestors
|
||||
CONTEXTS.PageContext = Class({
|
||||
extends: Context,
|
||||
|
||||
getState: function(popupNode) {
|
||||
// If there is a selection in the window then this context does not match
|
||||
if (!popupNode.ownerDocument.defaultView.getSelection().isCollapsed)
|
||||
return false;
|
||||
|
||||
// If the clicked node or any of its ancestors is one of the blocked
|
||||
// NON_PAGE_CONTEXT_ELTS then this context does not match
|
||||
while (!(popupNode instanceof Ci.nsIDOMDocument)) {
|
||||
if (NON_PAGE_CONTEXT_ELTS.some(type => popupNode instanceof type))
|
||||
return false;
|
||||
|
||||
popupNode = popupNode.parentNode;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Matches when there is an active selection in the window
|
||||
CONTEXTS.SelectionContext = Class({
|
||||
extends: Context,
|
||||
|
||||
getState: function(popupNode) {
|
||||
if (!popupNode.ownerDocument.defaultView.getSelection().isCollapsed)
|
||||
return true;
|
||||
|
||||
try {
|
||||
// The node may be a text box which has selectionStart and selectionEnd
|
||||
// properties. If not this will throw.
|
||||
let { selectionStart, selectionEnd } = popupNode;
|
||||
return !isNaN(selectionStart) && !isNaN(selectionEnd) &&
|
||||
selectionStart !== selectionEnd;
|
||||
}
|
||||
catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Matches when the context-clicked node or any of its ancestors matches the
|
||||
// selector given
|
||||
CONTEXTS.SelectorContext = Class({
|
||||
extends: Context,
|
||||
|
||||
initialize: function initialize(id, selector) {
|
||||
Context.prototype.initialize.call(this, id);
|
||||
this.selector = selector;
|
||||
},
|
||||
|
||||
adjustPopupNode: function adjustPopupNode(popupNode) {
|
||||
let selector = this.selector;
|
||||
|
||||
while (!(popupNode instanceof Ci.nsIDOMDocument)) {
|
||||
if (popupNode.matches(selector))
|
||||
return popupNode;
|
||||
|
||||
popupNode = popupNode.parentNode;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
getState: function(popupNode) {
|
||||
return !!this.adjustPopupNode(popupNode);
|
||||
}
|
||||
});
|
||||
|
||||
// Matches when the page url matches any of the patterns given
|
||||
CONTEXTS.URLContext = Class({
|
||||
extends: Context,
|
||||
|
||||
getState: function(popupNode) {
|
||||
return popupNode.ownerDocument.URL;
|
||||
}
|
||||
});
|
||||
|
||||
// Matches when the user-supplied predicate returns true
|
||||
CONTEXTS.PredicateContext = Class({
|
||||
extends: Context,
|
||||
|
||||
getState: function(node) {
|
||||
let window = node.ownerDocument.defaultView;
|
||||
let data = {};
|
||||
|
||||
data.documentType = node.ownerDocument.contentType;
|
||||
|
||||
data.documentURL = node.ownerDocument.location.href;
|
||||
data.targetName = node.nodeName.toLowerCase();
|
||||
data.targetID = node.id || null ;
|
||||
|
||||
if ((data.targetName === 'input' && editableInputs[node.type]) ||
|
||||
data.targetName === 'textarea') {
|
||||
data.isEditable = !node.readOnly && !node.disabled;
|
||||
}
|
||||
else {
|
||||
data.isEditable = node.isContentEditable;
|
||||
}
|
||||
|
||||
data.selectionText = getSelection(window, "TEXT");
|
||||
|
||||
data.srcURL = node.src || null;
|
||||
data.value = node.value || null;
|
||||
|
||||
while (!data.linkURL && node) {
|
||||
data.linkURL = node.href || null;
|
||||
node = node.parentNode;
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
function instantiateContext({ id, type, args }) {
|
||||
if (!(type in CONTEXTS)) {
|
||||
console.error("Attempt to use unknown context " + type);
|
||||
return;
|
||||
}
|
||||
return new CONTEXTS[type](id, ...args);
|
||||
}
|
||||
|
||||
var ContextWorker = Class({
|
||||
implements: [ WorkerChild ],
|
||||
|
||||
// Calls the context workers context listeners and returns the first result
|
||||
// that is either a string or a value that evaluates to true. If all of the
|
||||
// listeners returned false then returns false. If there are no listeners,
|
||||
// returns true (show the menu item by default).
|
||||
getMatchedContext: function getCurrentContexts(popupNode) {
|
||||
let results = this.sandbox.emitSync("context", popupNode);
|
||||
if (!results.length)
|
||||
return true;
|
||||
return results.reduce((val, result) => val || result);
|
||||
},
|
||||
|
||||
// Emits a click event in the worker's port. popupNode is the node that was
|
||||
// context-clicked, and clickedItemData is the data of the item that was
|
||||
// clicked.
|
||||
fireClick: function fireClick(popupNode, clickedItemData) {
|
||||
this.sandbox.emitSync("click", popupNode, clickedItemData);
|
||||
}
|
||||
});
|
||||
|
||||
// Gets the item's content script worker for a window, creating one if necessary
|
||||
// Once created it will be automatically destroyed when the window unloads.
|
||||
// If there is not content scripts for the item then null will be returned.
|
||||
function getItemWorkerForWindow(item, window) {
|
||||
if (!item.contentScript && !item.contentScriptFile)
|
||||
return null;
|
||||
|
||||
let id = getInnerId(window);
|
||||
let worker = item.workerMap.get(id);
|
||||
|
||||
if (worker)
|
||||
return worker;
|
||||
|
||||
worker = ContextWorker({
|
||||
id: item.id,
|
||||
window,
|
||||
manager: item.manager,
|
||||
contentScript: item.contentScript,
|
||||
contentScriptFile: item.contentScriptFile,
|
||||
onDetach: function() {
|
||||
item.workerMap.delete(id);
|
||||
}
|
||||
});
|
||||
|
||||
item.workerMap.set(id, worker);
|
||||
|
||||
return worker;
|
||||
}
|
||||
|
||||
// A very simple remote proxy for every item. It's job is to provide data for
|
||||
// the main process to use to determine visibility state and to call into
|
||||
// content scripts when clicked.
|
||||
var RemoteItem = Class({
|
||||
initialize: function(options, manager) {
|
||||
this.id = options.id;
|
||||
this.contexts = options.contexts.map(instantiateContext);
|
||||
this.contentScript = options.contentScript;
|
||||
this.contentScriptFile = options.contentScriptFile;
|
||||
|
||||
this.manager = manager;
|
||||
|
||||
this.workerMap = new Map();
|
||||
keepAlive.set(this.id, this);
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
for (let worker of this.workerMap.values()) {
|
||||
worker.destroy();
|
||||
}
|
||||
keepAlive.delete(this.id);
|
||||
},
|
||||
|
||||
activate: function(popupNode, data) {
|
||||
let worker = getItemWorkerForWindow(this, popupNode.ownerDocument.defaultView);
|
||||
if (!worker)
|
||||
return;
|
||||
|
||||
for (let context of this.contexts)
|
||||
popupNode = context.adjustPopupNode(popupNode);
|
||||
|
||||
worker.fireClick(popupNode, data);
|
||||
},
|
||||
|
||||
// Fills addonInfo with state data to send through to the main process
|
||||
getContextState: function(popupNode, addonInfo) {
|
||||
if (!(self.id in addonInfo)) {
|
||||
addonInfo[self.id] = {
|
||||
processID: process.id,
|
||||
items: {}
|
||||
};
|
||||
}
|
||||
|
||||
let worker = getItemWorkerForWindow(this, popupNode.ownerDocument.defaultView);
|
||||
let contextStates = {};
|
||||
for (let context of this.contexts)
|
||||
contextStates[context.id] = context.getState(popupNode);
|
||||
|
||||
addonInfo[self.id].items[this.id] = {
|
||||
// It isn't ideal to create a PageContext for every item but there isn't
|
||||
// a good shared place to do it.
|
||||
pageContext: (new CONTEXTS.PageContext()).getState(popupNode),
|
||||
contextStates,
|
||||
hasWorker: !!worker,
|
||||
workerContext: worker ? worker.getMatchedContext(popupNode) : true
|
||||
}
|
||||
}
|
||||
});
|
||||
exports.RemoteItem = RemoteItem;
|
||||
|
||||
// Holds remote items for this frame.
|
||||
var keepAlive = new Map();
|
||||
|
||||
// Called to create remote proxies for items. If they already exist we destroy
|
||||
// and recreate. This can happen if the item changes in some way or in odd
|
||||
// timing cases where the frame script is create around the same time as the
|
||||
// item is created in the main process
|
||||
process.port.on('sdk/contextmenu/createitems', (process, items) => {
|
||||
for (let itemoptions of items) {
|
||||
let oldItem = keepAlive.get(itemoptions.id);
|
||||
if (oldItem) {
|
||||
oldItem.destroy();
|
||||
}
|
||||
|
||||
let item = new RemoteItem(itemoptions, this);
|
||||
}
|
||||
});
|
||||
|
||||
process.port.on('sdk/contextmenu/destroyitems', (process, items) => {
|
||||
for (let id of items) {
|
||||
let item = keepAlive.get(id);
|
||||
item.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
var lastPopupNode = null;
|
||||
|
||||
system.on('content-contextmenu', ({ subject }) => {
|
||||
let { event: { target: popupNode }, addonInfo } = subject.wrappedJSObject;
|
||||
lastPopupNode = popupNode;
|
||||
|
||||
for (let item of keepAlive.values()) {
|
||||
item.getContextState(popupNode, addonInfo);
|
||||
}
|
||||
}, true);
|
||||
|
||||
process.port.on('sdk/contextmenu/activateitems', (process, items, data) => {
|
||||
for (let id of items) {
|
||||
let item = keepAlive.get(id);
|
||||
if (!item)
|
||||
continue;
|
||||
|
||||
item.activate(lastPopupNode, data);
|
||||
}
|
||||
});
|
||||
57
toolkit/jetpack/sdk/content/events.js
Normal file
57
toolkit/jetpack/sdk/content/events.js
Normal file
|
|
@ -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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const { Ci } = require("chrome");
|
||||
const { open } = require("../event/dom");
|
||||
const { observe } = require("../event/chrome");
|
||||
const { filter, merge, map, expand } = require("../event/utils");
|
||||
const { windows } = require("../window/utils");
|
||||
const { events: windowEvents } = require("sdk/window/events");
|
||||
|
||||
// Note: Please note that even though pagehide event is included
|
||||
// it's not observable reliably since it's not always triggered
|
||||
// when closing tabs. Implementation can be imrpoved once that
|
||||
// event will be necessary.
|
||||
var TYPES = ["DOMContentLoaded", "load", "pageshow", "pagehide"];
|
||||
|
||||
var insert = observe("document-element-inserted");
|
||||
var windowCreate = merge([
|
||||
observe("content-document-global-created"),
|
||||
observe("chrome-document-global-created")
|
||||
]);
|
||||
var create = map(windowCreate, function({target, data, type}) {
|
||||
return { target: target.document, type: type, data: data }
|
||||
});
|
||||
|
||||
function streamEventsFrom({document}) {
|
||||
// Map supported event types to a streams of those events on the given
|
||||
// `window` for the inserted document and than merge these streams into
|
||||
// single form stream off all window state change events.
|
||||
let stateChanges = TYPES.map(function(type) {
|
||||
return open(document, type, { capture: true });
|
||||
});
|
||||
|
||||
// Since load events on document occur for every loded resource
|
||||
return filter(merge(stateChanges), function({target}) {
|
||||
return target instanceof Ci.nsIDOMDocument
|
||||
})
|
||||
}
|
||||
exports.streamEventsFrom = streamEventsFrom;
|
||||
|
||||
var opened = windows(null, { includePrivate: true });
|
||||
var state = merge(opened.map(streamEventsFrom));
|
||||
|
||||
|
||||
var futureReady = filter(windowEvents, ({type}) =>
|
||||
type === "DOMContentLoaded");
|
||||
var futureWindows = map(futureReady, ({target}) => target);
|
||||
var futureState = expand(futureWindows, streamEventsFrom);
|
||||
|
||||
exports.events = merge([insert, create, state, futureState]);
|
||||
133
toolkit/jetpack/sdk/content/l10n-html.js
Normal file
133
toolkit/jetpack/sdk/content/l10n-html.js
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { Ci, Cc, Cu } = require("chrome");
|
||||
const core = require("../l10n/core");
|
||||
const { loadSheet, removeSheet } = require("../stylesheet/utils");
|
||||
const { process, frames } = require("../remote/child");
|
||||
var observerService = Cc["@mozilla.org/observer-service;1"]
|
||||
.getService(Ci.nsIObserverService);
|
||||
const { ShimWaiver } = Cu.import("resource://gre/modules/ShimWaiver.jsm");
|
||||
const addObserver = ShimWaiver.getProperty(observerService, "addObserver");
|
||||
const removeObserver = ShimWaiver.getProperty(observerService, "removeObserver");
|
||||
|
||||
const assetsURI = require('../self').data.url();
|
||||
|
||||
const hideSheetUri = "data:text/css,:root {visibility: hidden !important;}";
|
||||
|
||||
function translateElementAttributes(element) {
|
||||
// Translateable attributes
|
||||
const attrList = ['title', 'accesskey', 'alt', 'label', 'placeholder'];
|
||||
const ariaAttrMap = {
|
||||
'ariaLabel': 'aria-label',
|
||||
'ariaValueText': 'aria-valuetext',
|
||||
'ariaMozHint': 'aria-moz-hint'
|
||||
};
|
||||
const attrSeparator = '.';
|
||||
|
||||
// Try to translate each of the attributes
|
||||
for (let attribute of attrList) {
|
||||
const data = core.get(element.dataset.l10nId + attrSeparator + attribute);
|
||||
if (data)
|
||||
element.setAttribute(attribute, data);
|
||||
}
|
||||
|
||||
// Look for the aria attribute translations that match fxOS's aliases
|
||||
for (let attrAlias in ariaAttrMap) {
|
||||
const data = core.get(element.dataset.l10nId + attrSeparator + attrAlias);
|
||||
if (data)
|
||||
element.setAttribute(ariaAttrMap[attrAlias], data);
|
||||
}
|
||||
}
|
||||
|
||||
// Taken from Gaia:
|
||||
// https://github.com/andreasgal/gaia/blob/04fde2640a7f40314643016a5a6c98bf3755f5fd/webapi.js#L1470
|
||||
function translateElement(element) {
|
||||
element = element || document;
|
||||
|
||||
// check all translatable children (= w/ a `data-l10n-id' attribute)
|
||||
var children = element.querySelectorAll('*[data-l10n-id]');
|
||||
var elementCount = children.length;
|
||||
for (var i = 0; i < elementCount; i++) {
|
||||
var child = children[i];
|
||||
|
||||
// translate the child
|
||||
var key = child.dataset.l10nId;
|
||||
var data = core.get(key);
|
||||
if (data)
|
||||
child.textContent = data;
|
||||
|
||||
translateElementAttributes(child);
|
||||
}
|
||||
}
|
||||
exports.translateElement = translateElement;
|
||||
|
||||
function onDocumentReady2Translate(event) {
|
||||
let document = event.target;
|
||||
document.removeEventListener("DOMContentLoaded", onDocumentReady2Translate,
|
||||
false);
|
||||
|
||||
translateElement(document);
|
||||
|
||||
try {
|
||||
// Finally display document when we finished replacing all text content
|
||||
if (document.defaultView)
|
||||
removeSheet(document.defaultView, hideSheetUri, 'user');
|
||||
}
|
||||
catch(e) {
|
||||
console.exception(e);
|
||||
}
|
||||
}
|
||||
|
||||
function onContentWindow(document) {
|
||||
// Accept only HTML documents
|
||||
if (!(document instanceof Ci.nsIDOMHTMLDocument))
|
||||
return;
|
||||
|
||||
// Bug 769483: data:URI documents instanciated with nsIDOMParser
|
||||
// have a null `location` attribute at this time
|
||||
if (!document.location)
|
||||
return;
|
||||
|
||||
// Accept only document from this addon
|
||||
if (document.location.href.indexOf(assetsURI) !== 0)
|
||||
return;
|
||||
|
||||
try {
|
||||
// First hide content of the document in order to have content blinking
|
||||
// between untranslated and translated states
|
||||
loadSheet(document.defaultView, hideSheetUri, 'user');
|
||||
}
|
||||
catch(e) {
|
||||
console.exception(e);
|
||||
}
|
||||
// Wait for DOM tree to be built before applying localization
|
||||
document.addEventListener("DOMContentLoaded", onDocumentReady2Translate,
|
||||
false);
|
||||
}
|
||||
|
||||
// Listen to creation of content documents in order to translate them as soon
|
||||
// as possible in their loading process
|
||||
const ON_CONTENT = "document-element-inserted";
|
||||
let enabled = false;
|
||||
function enable() {
|
||||
if (enabled)
|
||||
return;
|
||||
addObserver(onContentWindow, ON_CONTENT, false);
|
||||
enabled = true;
|
||||
}
|
||||
process.port.on("sdk/l10n/html/enable", enable);
|
||||
|
||||
function disable() {
|
||||
if (!enabled)
|
||||
return;
|
||||
removeObserver(onContentWindow, ON_CONTENT);
|
||||
enabled = false;
|
||||
}
|
||||
process.port.on("sdk/l10n/html/disable", disable);
|
||||
74
toolkit/jetpack/sdk/content/loader.js
Normal file
74
toolkit/jetpack/sdk/content/loader.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { isValidURI, isLocalURL, URL } = require('../url');
|
||||
const { contract } = require('../util/contract');
|
||||
const { isString, isNil, instanceOf, isJSONable } = require('../lang/type');
|
||||
const { validateOptions,
|
||||
string, array, object, either, required } = require('../deprecated/api-utils');
|
||||
|
||||
const isValidScriptFile = (value) =>
|
||||
(isString(value) || instanceOf(value, URL)) && isLocalURL(value);
|
||||
|
||||
// map of property validations
|
||||
const valid = {
|
||||
contentURL: {
|
||||
is: either(string, object),
|
||||
ok: url => isNil(url) || isLocalURL(url) || isValidURI(url),
|
||||
msg: 'The `contentURL` option must be a valid URL.'
|
||||
},
|
||||
contentScriptFile: {
|
||||
is: either(string, object, array),
|
||||
ok: value => isNil(value) || [].concat(value).every(isValidScriptFile),
|
||||
msg: 'The `contentScriptFile` option must be a local URL or an array of URLs.'
|
||||
},
|
||||
contentScript: {
|
||||
is: either(string, array),
|
||||
ok: value => isNil(value) || [].concat(value).every(isString),
|
||||
msg: 'The `contentScript` option must be a string or an array of strings.'
|
||||
},
|
||||
contentScriptWhen: {
|
||||
is: required(string),
|
||||
map: value => value || 'end',
|
||||
ok: value => ~['start', 'ready', 'end'].indexOf(value),
|
||||
msg: 'The `contentScriptWhen` option must be either "start", "ready" or "end".'
|
||||
},
|
||||
contentScriptOptions: {
|
||||
ok: value => isNil(value) || isJSONable(value),
|
||||
msg: 'The contentScriptOptions should be a jsonable value.'
|
||||
}
|
||||
};
|
||||
exports.validationAttributes = valid;
|
||||
|
||||
/**
|
||||
* Shortcut function to validate property with validation.
|
||||
* @param {Object|Number|String} suspect
|
||||
* value to validate
|
||||
* @param {Object} validation
|
||||
* validation rule passed to `api-utils`
|
||||
*/
|
||||
function validate(suspect, validation) {
|
||||
return validateOptions(
|
||||
{ $: suspect },
|
||||
{ $: validation }
|
||||
).$;
|
||||
}
|
||||
|
||||
function Allow(script) {
|
||||
return {
|
||||
get script() {
|
||||
return script;
|
||||
},
|
||||
set script(value) {
|
||||
script = !!value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
exports.contract = contract(valid);
|
||||
68
toolkit/jetpack/sdk/content/mod.js
Normal file
68
toolkit/jetpack/sdk/content/mod.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const { Ci } = require("chrome");
|
||||
const { dispatcher } = require("../util/dispatcher");
|
||||
const { add, remove, iterator } = require("../lang/weak-set");
|
||||
|
||||
var getTargetWindow = dispatcher("getTargetWindow");
|
||||
|
||||
getTargetWindow.define(function (target) {
|
||||
if (target instanceof Ci.nsIDOMWindow)
|
||||
return target;
|
||||
if (target instanceof Ci.nsIDOMDocument)
|
||||
return target.defaultView || null;
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
exports.getTargetWindow = getTargetWindow;
|
||||
|
||||
var attachTo = dispatcher("attachTo");
|
||||
exports.attachTo = attachTo;
|
||||
|
||||
var detachFrom = dispatcher("detatchFrom");
|
||||
exports.detachFrom = detachFrom;
|
||||
|
||||
function attach(modification, target) {
|
||||
if (!modification)
|
||||
return;
|
||||
|
||||
let window = getTargetWindow(target);
|
||||
|
||||
attachTo(modification, window);
|
||||
|
||||
// modification are stored per content; `window` reference can still be the
|
||||
// same even if the content is changed, therefore `document` is used instead.
|
||||
add(modification, window.document);
|
||||
}
|
||||
exports.attach = attach;
|
||||
|
||||
function detach(modification, target) {
|
||||
if (!modification)
|
||||
return;
|
||||
|
||||
if (target) {
|
||||
let window = getTargetWindow(target);
|
||||
detachFrom(modification, window);
|
||||
remove(modification, window.document);
|
||||
}
|
||||
else {
|
||||
let documents = iterator(modification);
|
||||
for (let document of documents) {
|
||||
let window = document.defaultView;
|
||||
// The window might have already gone away
|
||||
if (!window)
|
||||
continue;
|
||||
detachFrom(modification, document.defaultView);
|
||||
remove(modification, document);
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.detach = detach;
|
||||
236
toolkit/jetpack/sdk/content/page-mod.js
Normal file
236
toolkit/jetpack/sdk/content/page-mod.js
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "stable"
|
||||
};
|
||||
|
||||
const { getAttachEventType } = require('../content/utils');
|
||||
const { Class } = require('../core/heritage');
|
||||
const { Disposable } = require('../core/disposable');
|
||||
const { WeakReference } = require('../core/reference');
|
||||
const { WorkerChild } = require('./worker-child');
|
||||
const { EventTarget } = require('../event/target');
|
||||
const { on, emit, once, setListeners } = require('../event/core');
|
||||
const { on: domOn, removeListener: domOff } = require('../dom/events');
|
||||
const { isRegExp, isUndefined } = require('../lang/type');
|
||||
const { merge } = require('../util/object');
|
||||
const { isBrowser, getFrames } = require('../window/utils');
|
||||
const { getTabs, getURI: getTabURI } = require('../tabs/utils');
|
||||
const { ignoreWindow } = require('../private-browsing/utils');
|
||||
const { Style } = require("../stylesheet/style");
|
||||
const { attach, detach } = require("../content/mod");
|
||||
const { has, hasAny } = require("../util/array");
|
||||
const { Rules } = require("../util/rules");
|
||||
const { List, addListItem, removeListItem } = require('../util/list');
|
||||
const { when } = require("../system/unload");
|
||||
const { uuid } = require('../util/uuid');
|
||||
const { frames, process } = require('../remote/child');
|
||||
|
||||
const pagemods = new Map();
|
||||
const styles = new WeakMap();
|
||||
var styleFor = (mod) => styles.get(mod);
|
||||
|
||||
// Helper functions
|
||||
var modMatchesURI = (mod, uri) => mod.include.matchesAny(uri) && !mod.exclude.matchesAny(uri);
|
||||
|
||||
/**
|
||||
* PageMod constructor (exported below).
|
||||
* @constructor
|
||||
*/
|
||||
const ChildPageMod = Class({
|
||||
implements: [
|
||||
EventTarget,
|
||||
Disposable,
|
||||
],
|
||||
setup: function PageMod(model) {
|
||||
merge(this, model);
|
||||
|
||||
// Set listeners on {PageMod} itself, not the underlying worker,
|
||||
// like `onMessage`, as it'll get piped.
|
||||
setListeners(this, model);
|
||||
|
||||
function deserializeRules(rules) {
|
||||
for (let rule of rules) {
|
||||
yield rule.type == "string" ? rule.value
|
||||
: new RegExp(rule.pattern, rule.flags);
|
||||
}
|
||||
}
|
||||
|
||||
let include = [...deserializeRules(this.include)];
|
||||
this.include = Rules();
|
||||
this.include.add.apply(this.include, include);
|
||||
|
||||
let exclude = [...deserializeRules(this.exclude)];
|
||||
this.exclude = Rules();
|
||||
this.exclude.add.apply(this.exclude, exclude);
|
||||
|
||||
if (this.contentStyle || this.contentStyleFile) {
|
||||
styles.set(this, Style({
|
||||
uri: this.contentStyleFile,
|
||||
source: this.contentStyle
|
||||
}));
|
||||
}
|
||||
|
||||
pagemods.set(this.id, this);
|
||||
this.seenDocuments = new WeakMap();
|
||||
|
||||
// `applyOnExistingDocuments` has to be called after `pagemods.add()`
|
||||
// otherwise its calls to `onContent` method won't do anything.
|
||||
if (has(this.attachTo, 'existing'))
|
||||
applyOnExistingDocuments(this);
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
let style = styleFor(this);
|
||||
if (style)
|
||||
detach(style);
|
||||
|
||||
for (let i in this.include)
|
||||
this.include.remove(this.include[i]);
|
||||
|
||||
pagemods.delete(this.id);
|
||||
}
|
||||
});
|
||||
|
||||
function onContentWindow({ target: document }) {
|
||||
// Return if we have no pagemods
|
||||
if (pagemods.size === 0)
|
||||
return;
|
||||
|
||||
let window = document.defaultView;
|
||||
// XML documents don't have windows, and we don't yet support them.
|
||||
if (!window)
|
||||
return;
|
||||
|
||||
// Frame event listeners are bound to the frame the event came from by default
|
||||
let frame = this;
|
||||
// We apply only on documents in tabs of Firefox
|
||||
if (!frame.isTab)
|
||||
return;
|
||||
|
||||
// When the tab is private, only addons with 'private-browsing' flag in
|
||||
// their package.json can apply content script to private documents
|
||||
if (ignoreWindow(window))
|
||||
return;
|
||||
|
||||
for (let pagemod of pagemods.values()) {
|
||||
if (modMatchesURI(pagemod, window.location.href))
|
||||
onContent(pagemod, window);
|
||||
}
|
||||
}
|
||||
frames.addEventListener("DOMDocElementInserted", onContentWindow, true);
|
||||
|
||||
function applyOnExistingDocuments (mod) {
|
||||
for (let frame of frames) {
|
||||
// Fake a newly created document
|
||||
let window = frame.content;
|
||||
// on startup with e10s, contentWindow might not exist yet,
|
||||
// in which case we will get notified by "document-element-inserted".
|
||||
if (!window || !window.frames)
|
||||
return;
|
||||
let uri = window.location.href;
|
||||
if (has(mod.attachTo, "top") && modMatchesURI(mod, uri))
|
||||
onContent(mod, window);
|
||||
if (has(mod.attachTo, "frame"))
|
||||
getFrames(window).
|
||||
filter(iframe => modMatchesURI(mod, iframe.location.href)).
|
||||
forEach(frame => onContent(mod, frame));
|
||||
}
|
||||
}
|
||||
|
||||
function createWorker(mod, window) {
|
||||
let workerId = String(uuid());
|
||||
|
||||
// Instruct the parent to connect to this worker. Do this first so the parent
|
||||
// side is connected before the worker attempts to send any messages there
|
||||
let frame = frames.getFrameForWindow(window.top);
|
||||
frame.port.emit('sdk/page-mod/worker-create', mod.id, {
|
||||
id: workerId,
|
||||
url: window.location.href
|
||||
});
|
||||
|
||||
// Create a child worker and notify the parent
|
||||
let worker = WorkerChild({
|
||||
id: workerId,
|
||||
window: window,
|
||||
contentScript: mod.contentScript,
|
||||
contentScriptFile: mod.contentScriptFile,
|
||||
contentScriptOptions: mod.contentScriptOptions
|
||||
});
|
||||
|
||||
once(worker, 'detach', () => worker.destroy());
|
||||
}
|
||||
|
||||
function onContent (mod, window) {
|
||||
let isTopDocument = window.top === window;
|
||||
// Is a top level document and `top` is not set, ignore
|
||||
if (isTopDocument && !has(mod.attachTo, "top"))
|
||||
return;
|
||||
// Is a frame document and `frame` is not set, ignore
|
||||
if (!isTopDocument && !has(mod.attachTo, "frame"))
|
||||
return;
|
||||
|
||||
// ensure we attach only once per document
|
||||
let seen = mod.seenDocuments;
|
||||
if (seen.has(window.document))
|
||||
return;
|
||||
seen.set(window.document, true);
|
||||
|
||||
let style = styleFor(mod);
|
||||
if (style)
|
||||
attach(style, window);
|
||||
|
||||
// Immediately evaluate content script if the document state is already
|
||||
// matching contentScriptWhen expectations
|
||||
if (isMatchingAttachState(mod, window)) {
|
||||
createWorker(mod, window);
|
||||
return;
|
||||
}
|
||||
|
||||
let eventName = getAttachEventType(mod) || 'load';
|
||||
domOn(window, eventName, function onReady (e) {
|
||||
if (e.target.defaultView !== window)
|
||||
return;
|
||||
domOff(window, eventName, onReady, true);
|
||||
createWorker(mod, window);
|
||||
|
||||
// Attaching is asynchronous so if the document is already loaded we will
|
||||
// miss the pageshow event so send a synthetic one.
|
||||
if (window.document.readyState == "complete") {
|
||||
mod.on('attach', worker => {
|
||||
try {
|
||||
worker.send('pageshow');
|
||||
emit(worker, 'pageshow');
|
||||
}
|
||||
catch (e) {
|
||||
// This can fail if an earlier attach listener destroyed the worker
|
||||
}
|
||||
});
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
function isMatchingAttachState (mod, window) {
|
||||
let state = window.document.readyState;
|
||||
return 'start' === mod.contentScriptWhen ||
|
||||
// Is `load` event already dispatched?
|
||||
'complete' === state ||
|
||||
// Is DOMContentLoaded already dispatched and waiting for it?
|
||||
('ready' === mod.contentScriptWhen && state === 'interactive')
|
||||
}
|
||||
|
||||
process.port.on('sdk/page-mod/create', (process, model) => {
|
||||
if (pagemods.has(model.id))
|
||||
return;
|
||||
|
||||
new ChildPageMod(model);
|
||||
});
|
||||
|
||||
process.port.on('sdk/page-mod/destroy', (process, id) => {
|
||||
let mod = pagemods.get(id);
|
||||
if (mod)
|
||||
mod.destroy();
|
||||
});
|
||||
154
toolkit/jetpack/sdk/content/page-worker.js
Normal file
154
toolkit/jetpack/sdk/content/page-worker.js
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
/* 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 { frames } = require("../remote/child");
|
||||
const { Class } = require("../core/heritage");
|
||||
const { Disposable } = require('../core/disposable');
|
||||
const { data } = require("../self");
|
||||
const { once } = require("../dom/events");
|
||||
const { getAttachEventType } = require("./utils");
|
||||
const { Rules } = require('../util/rules');
|
||||
const { uuid } = require('../util/uuid');
|
||||
const { WorkerChild } = require("./worker-child");
|
||||
const { Cc, Ci, Cu } = require("chrome");
|
||||
const { observe } = require("../event/chrome");
|
||||
const { on } = require("../event/core");
|
||||
|
||||
const appShell = Cc["@mozilla.org/appshell/appShellService;1"].getService(Ci.nsIAppShellService);
|
||||
|
||||
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
const pages = new Map();
|
||||
|
||||
const DOC_INSERTED = "document-element-inserted";
|
||||
|
||||
function isValidURL(page, url) {
|
||||
return !page.rules || page.rules.matchesAny(url);
|
||||
}
|
||||
|
||||
const ChildPage = Class({
|
||||
implements: [ Disposable ],
|
||||
setup: function(frame, id, options) {
|
||||
this.id = id;
|
||||
this.frame = frame;
|
||||
this.options = options;
|
||||
|
||||
this.webNav = appShell.createWindowlessBrowser(false);
|
||||
this.docShell.allowJavascript = this.options.allow.script;
|
||||
|
||||
// Accessing the browser's window forces the initial about:blank document to
|
||||
// be created before we start listening for notifications
|
||||
this.contentWindow;
|
||||
|
||||
this.webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
|
||||
|
||||
pages.set(this.id, this);
|
||||
|
||||
this.contentURL = options.contentURL;
|
||||
|
||||
if (options.include) {
|
||||
this.rules = Rules();
|
||||
this.rules.add.apply(this.rules, [].concat(options.include));
|
||||
}
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
pages.delete(this.id);
|
||||
this.webProgress.removeProgressListener(this);
|
||||
this.webNav.close();
|
||||
this.webNav = null;
|
||||
},
|
||||
|
||||
attachWorker: function() {
|
||||
if (!isValidURL(this, this.contentWindow.location.href))
|
||||
return;
|
||||
|
||||
this.options.id = uuid().toString();
|
||||
this.options.window = this.contentWindow;
|
||||
this.frame.port.emit("sdk/frame/connect", this.id, {
|
||||
id: this.options.id,
|
||||
url: this.contentWindow.document.documentURIObject.spec
|
||||
});
|
||||
new WorkerChild(this.options);
|
||||
},
|
||||
|
||||
get docShell() {
|
||||
return this.webNav.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDocShell);
|
||||
},
|
||||
|
||||
get webProgress() {
|
||||
return this.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebProgress);
|
||||
},
|
||||
|
||||
get contentWindow() {
|
||||
return this.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindow);
|
||||
},
|
||||
|
||||
get contentURL() {
|
||||
return this.options.contentURL;
|
||||
},
|
||||
set contentURL(url) {
|
||||
this.options.contentURL = url;
|
||||
|
||||
url = this.options.contentURL ? data.url(this.options.contentURL) : "about:blank";
|
||||
this.webNav.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null, null);
|
||||
},
|
||||
|
||||
onLocationChange: function(progress, request, location, flags) {
|
||||
// Ignore inner-frame events
|
||||
if (progress != this.webProgress)
|
||||
return;
|
||||
// Ignore events that don't change the document
|
||||
if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
|
||||
return;
|
||||
|
||||
let event = getAttachEventType(this.options);
|
||||
// Attaching at the start of the load is handled by the
|
||||
// document-element-inserted listener.
|
||||
if (event == DOC_INSERTED)
|
||||
return;
|
||||
|
||||
once(this.contentWindow, event, () => {
|
||||
this.attachWorker();
|
||||
}, false);
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI(["nsIWebProgressListener", "nsISupportsWeakReference"])
|
||||
});
|
||||
|
||||
on(observe(DOC_INSERTED), "data", ({ target }) => {
|
||||
let page = Array.from(pages.values()).find(p => p.contentWindow.document === target);
|
||||
if (!page)
|
||||
return;
|
||||
|
||||
if (getAttachEventType(page.options) == DOC_INSERTED)
|
||||
page.attachWorker();
|
||||
});
|
||||
|
||||
frames.port.on("sdk/frame/create", (frame, id, options) => {
|
||||
new ChildPage(frame, id, options);
|
||||
});
|
||||
|
||||
frames.port.on("sdk/frame/set", (frame, id, params) => {
|
||||
let page = pages.get(id);
|
||||
if (!page)
|
||||
return;
|
||||
|
||||
if ("allowScript" in params)
|
||||
page.docShell.allowJavascript = params.allowScript;
|
||||
if ("contentURL" in params)
|
||||
page.contentURL = params.contentURL;
|
||||
});
|
||||
|
||||
frames.port.on("sdk/frame/destroy", (frame, id) => {
|
||||
let page = pages.get(id);
|
||||
if (!page)
|
||||
return;
|
||||
|
||||
page.destroy();
|
||||
});
|
||||
426
toolkit/jetpack/sdk/content/sandbox.js
Normal file
426
toolkit/jetpack/sdk/content/sandbox.js
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
/* 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';
|
||||
|
||||
module.metadata = {
|
||||
'stability': 'unstable'
|
||||
};
|
||||
|
||||
const { Class } = require('../core/heritage');
|
||||
const { EventTarget } = require('../event/target');
|
||||
const { on, off, emit } = require('../event/core');
|
||||
const { events } = require('./sandbox/events');
|
||||
const { requiresAddonGlobal } = require('./utils');
|
||||
const { delay: async } = require('../lang/functional');
|
||||
const { Ci, Cu, Cc } = require('chrome');
|
||||
const timer = require('../timers');
|
||||
const { URL } = require('../url');
|
||||
const { sandbox, evaluate, load } = require('../loader/sandbox');
|
||||
const { merge } = require('../util/object');
|
||||
const { getTabForContentWindowNoShim } = require('../tabs/utils');
|
||||
const { getInnerId } = require('../window/utils');
|
||||
const { PlainTextConsole } = require('../console/plain-text');
|
||||
const { data } = require('../self');const { isChildLoader } = require('../remote/core');
|
||||
// WeakMap of sandboxes so we can access private values
|
||||
const sandboxes = new WeakMap();
|
||||
|
||||
/* Trick the linker in order to ensure shipping these files in the XPI.
|
||||
require('./content-worker.js');
|
||||
Then, retrieve URL of these files in the XPI:
|
||||
*/
|
||||
var prefix = module.uri.split('sandbox.js')[0];
|
||||
const CONTENT_WORKER_URL = prefix + 'content-worker.js';
|
||||
const metadata = require('@loader/options').metadata;
|
||||
|
||||
// Fetch additional list of domains to authorize access to for each content
|
||||
// script. It is stored in manifest `metadata` field which contains
|
||||
// package.json data. This list is originaly defined by authors in
|
||||
// `permissions` attribute of their package.json addon file.
|
||||
const permissions = (metadata && metadata['permissions']) || {};
|
||||
const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || [];
|
||||
|
||||
const waiveSecurityMembrane = !!permissions['unsafe-content-script'];
|
||||
|
||||
const nsIScriptSecurityManager = Ci.nsIScriptSecurityManager;
|
||||
const secMan = Cc["@mozilla.org/scriptsecuritymanager;1"].
|
||||
getService(Ci.nsIScriptSecurityManager);
|
||||
|
||||
const JS_VERSION = '1.8';
|
||||
|
||||
// Tests whether this window is loaded in a tab
|
||||
function isWindowInTab(window) {
|
||||
if (isChildLoader) {
|
||||
let { frames } = require('../remote/child');
|
||||
let frame = frames.getFrameForWindow(window.top);
|
||||
return frame && frame.isTab;
|
||||
}
|
||||
else {
|
||||
// The deprecated sync worker API still does everything in the main process
|
||||
return getTabForContentWindowNoShim(window);
|
||||
}
|
||||
}
|
||||
|
||||
const WorkerSandbox = Class({
|
||||
implements: [ EventTarget ],
|
||||
|
||||
/**
|
||||
* Emit a message to the worker content sandbox
|
||||
*/
|
||||
emit: function emit(type, ...args) {
|
||||
// JSON.stringify is buggy with cross-sandbox values,
|
||||
// it may return "{}" on functions. Use a replacer to match them correctly.
|
||||
let replacer = (k, v) =>
|
||||
typeof(v) === "function"
|
||||
? (type === "console" ? Function.toString.call(v) : void(0))
|
||||
: v;
|
||||
|
||||
// Ensure having an asynchronous behavior
|
||||
async(() =>
|
||||
emitToContent(this, JSON.stringify([type, ...args], replacer))
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Synchronous version of `emit`.
|
||||
* /!\ Should only be used when it is strictly mandatory /!\
|
||||
* Doesn't ensure passing only JSON values.
|
||||
* Mainly used by context-menu in order to avoid breaking it.
|
||||
*/
|
||||
emitSync: function emitSync(...args) {
|
||||
// because the arguments could be also non JSONable values,
|
||||
// we need to ensure the array instance is created from
|
||||
// the content's sandbox
|
||||
return emitToContent(this, new modelFor(this).sandbox.Array(...args));
|
||||
},
|
||||
|
||||
/**
|
||||
* Configures sandbox and loads content scripts into it.
|
||||
* @param {Worker} worker
|
||||
* content worker
|
||||
*/
|
||||
initialize: function WorkerSandbox(worker, window) {
|
||||
let model = {};
|
||||
sandboxes.set(this, model);
|
||||
model.worker = worker;
|
||||
// We receive a wrapped window, that may be an xraywrapper if it's content
|
||||
let proto = window;
|
||||
|
||||
// TODO necessary?
|
||||
// Ensure that `emit` has always the right `this`
|
||||
this.emit = this.emit.bind(this);
|
||||
this.emitSync = this.emitSync.bind(this);
|
||||
|
||||
// Use expanded principal for content-script if the content is a
|
||||
// regular web content for better isolation.
|
||||
// (This behavior can be turned off for now with the unsafe-content-script
|
||||
// flag to give addon developers time for making the necessary changes)
|
||||
// But prevent it when the Worker isn't used for a content script but for
|
||||
// injecting `addon` object into a Panel scope, for example.
|
||||
// That's because:
|
||||
// 1/ It is useless to use multiple domains as the worker is only used
|
||||
// to communicate with the addon,
|
||||
// 2/ By using it it would prevent the document to have access to any JS
|
||||
// value of the worker. As JS values coming from multiple domain principals
|
||||
// can't be accessed by 'mono-principals' (principal with only one domain).
|
||||
// Even if this principal is for a domain that is specified in the multiple
|
||||
// domain principal.
|
||||
let principals = window;
|
||||
let wantGlobalProperties = [];
|
||||
let isSystemPrincipal = secMan.isSystemPrincipal(
|
||||
window.document.nodePrincipal);
|
||||
if (!isSystemPrincipal && !requiresAddonGlobal(worker)) {
|
||||
if (EXPANDED_PRINCIPALS.length > 0) {
|
||||
// We have to replace XHR constructor of the content document
|
||||
// with a custom cross origin one, automagically added by platform code:
|
||||
delete proto.XMLHttpRequest;
|
||||
wantGlobalProperties.push('XMLHttpRequest');
|
||||
}
|
||||
if (!waiveSecurityMembrane)
|
||||
principals = EXPANDED_PRINCIPALS.concat(window);
|
||||
}
|
||||
|
||||
// Create the sandbox and bind it to window in order for content scripts to
|
||||
// have access to all standard globals (window, document, ...)
|
||||
let content = sandbox(principals, {
|
||||
sandboxPrototype: proto,
|
||||
wantXrays: !requiresAddonGlobal(worker),
|
||||
wantGlobalProperties: wantGlobalProperties,
|
||||
wantExportHelpers: true,
|
||||
sameZoneAs: window,
|
||||
metadata: {
|
||||
SDKContentScript: true,
|
||||
'inner-window-id': getInnerId(window)
|
||||
}
|
||||
});
|
||||
model.sandbox = content;
|
||||
|
||||
// We have to ensure that window.top and window.parent are the exact same
|
||||
// object than window object, i.e. the sandbox global object. But not
|
||||
// always, in case of iframes, top and parent are another window object.
|
||||
let top = window.top === window ? content : content.top;
|
||||
let parent = window.parent === window ? content : content.parent;
|
||||
merge(content, {
|
||||
// We need 'this === window === top' to be true in toplevel scope:
|
||||
get window() {
|
||||
return content;
|
||||
},
|
||||
get top() {
|
||||
return top;
|
||||
},
|
||||
get parent() {
|
||||
return parent;
|
||||
}
|
||||
});
|
||||
|
||||
// Use the Greasemonkey naming convention to provide access to the
|
||||
// unwrapped window object so the content script can access document
|
||||
// JavaScript values.
|
||||
// NOTE: this functionality is experimental and may change or go away
|
||||
// at any time!
|
||||
//
|
||||
// Note that because waivers aren't propagated between origins, we
|
||||
// need the unsafeWindow getter to live in the sandbox.
|
||||
var unsafeWindowGetter =
|
||||
new content.Function('return window.wrappedJSObject || window;');
|
||||
Object.defineProperty(content, 'unsafeWindow', {get: unsafeWindowGetter});
|
||||
|
||||
// Load trusted code that will inject content script API.
|
||||
let ContentWorker = load(content, CONTENT_WORKER_URL);
|
||||
|
||||
// prepare a clean `self.options`
|
||||
let options = 'contentScriptOptions' in worker ?
|
||||
JSON.stringify(worker.contentScriptOptions) :
|
||||
undefined;
|
||||
|
||||
// Then call `inject` method and communicate with this script
|
||||
// by trading two methods that allow to send events to the other side:
|
||||
// - `onEvent` called by content script
|
||||
// - `result.emitToContent` called by addon script
|
||||
let onEvent = Cu.exportFunction(onContentEvent.bind(null, this), ContentWorker);
|
||||
let chromeAPI = createChromeAPI(ContentWorker);
|
||||
let result = Cu.waiveXrays(ContentWorker).inject(content, chromeAPI, onEvent, options);
|
||||
|
||||
// Merge `emitToContent` into our private model of the
|
||||
// WorkerSandbox so we can communicate with content script
|
||||
model.emitToContent = result;
|
||||
|
||||
let console = new PlainTextConsole(null, getInnerId(window));
|
||||
|
||||
// Handle messages send by this script:
|
||||
setListeners(this, console);
|
||||
|
||||
// Inject `addon` global into target document if document is trusted,
|
||||
// `addon` in document is equivalent to `self` in content script.
|
||||
if (requiresAddonGlobal(worker)) {
|
||||
Object.defineProperty(getUnsafeWindow(window), 'addon', {
|
||||
value: content.self,
|
||||
configurable: true
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Inject our `console` into target document if worker doesn't have a tab
|
||||
// (e.g Panel, PageWorker).
|
||||
// `worker.tab` can't be used because bug 804935.
|
||||
if (!isWindowInTab(window)) {
|
||||
let win = getUnsafeWindow(window);
|
||||
|
||||
// export our chrome console to content window, as described here:
|
||||
// https://developer.mozilla.org/en-US/docs/Components.utils.createObjectIn
|
||||
let con = Cu.createObjectIn(win);
|
||||
|
||||
let genPropDesc = function genPropDesc(fun) {
|
||||
return { enumerable: true, configurable: true, writable: true,
|
||||
value: console[fun] };
|
||||
}
|
||||
|
||||
const properties = {
|
||||
log: genPropDesc('log'),
|
||||
info: genPropDesc('info'),
|
||||
warn: genPropDesc('warn'),
|
||||
error: genPropDesc('error'),
|
||||
debug: genPropDesc('debug'),
|
||||
trace: genPropDesc('trace'),
|
||||
dir: genPropDesc('dir'),
|
||||
group: genPropDesc('group'),
|
||||
groupCollapsed: genPropDesc('groupCollapsed'),
|
||||
groupEnd: genPropDesc('groupEnd'),
|
||||
time: genPropDesc('time'),
|
||||
timeEnd: genPropDesc('timeEnd'),
|
||||
profile: genPropDesc('profile'),
|
||||
profileEnd: genPropDesc('profileEnd'),
|
||||
exception: genPropDesc('exception'),
|
||||
assert: genPropDesc('assert'),
|
||||
count: genPropDesc('count'),
|
||||
table: genPropDesc('table'),
|
||||
clear: genPropDesc('clear'),
|
||||
dirxml: genPropDesc('dirxml'),
|
||||
markTimeline: genPropDesc('markTimeline'),
|
||||
timeline: genPropDesc('timeline'),
|
||||
timelineEnd: genPropDesc('timelineEnd'),
|
||||
timeStamp: genPropDesc('timeStamp'),
|
||||
};
|
||||
|
||||
Object.defineProperties(con, properties);
|
||||
Cu.makeObjectPropsNormal(con);
|
||||
|
||||
win.console = con;
|
||||
};
|
||||
|
||||
emit(events, "content-script-before-inserted", {
|
||||
window: window,
|
||||
worker: worker
|
||||
});
|
||||
|
||||
// The order of `contentScriptFile` and `contentScript` evaluation is
|
||||
// intentional, so programs can load libraries like jQuery from script URLs
|
||||
// and use them in scripts.
|
||||
let contentScriptFile = ('contentScriptFile' in worker)
|
||||
? worker.contentScriptFile
|
||||
: null,
|
||||
contentScript = ('contentScript' in worker)
|
||||
? worker.contentScript
|
||||
: null;
|
||||
|
||||
if (contentScriptFile)
|
||||
importScripts.apply(null, [this].concat(contentScriptFile));
|
||||
|
||||
if (contentScript) {
|
||||
evaluateIn(
|
||||
this,
|
||||
Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript
|
||||
);
|
||||
}
|
||||
},
|
||||
destroy: function destroy(reason) {
|
||||
if (typeof reason != 'string')
|
||||
reason = '';
|
||||
this.emitSync('event', 'detach', reason);
|
||||
let model = modelFor(this);
|
||||
model.sandbox = null
|
||||
model.worker = null;
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
exports.WorkerSandbox = WorkerSandbox;
|
||||
|
||||
/**
|
||||
* Imports scripts to the sandbox by reading files under urls and
|
||||
* evaluating its source. If exception occurs during evaluation
|
||||
* `'error'` event is emitted on the worker.
|
||||
* This is actually an analog to the `importScript` method in web
|
||||
* workers but in our case it's not exposed even though content
|
||||
* scripts may be able to do it synchronously since IO operation
|
||||
* takes place in the UI process.
|
||||
*/
|
||||
function importScripts (workerSandbox, ...urls) {
|
||||
let { worker, sandbox } = modelFor(workerSandbox);
|
||||
for (let i in urls) {
|
||||
let contentScriptFile = data.url(urls[i]);
|
||||
|
||||
try {
|
||||
let uri = URL(contentScriptFile);
|
||||
if (uri.scheme === 'resource')
|
||||
load(sandbox, String(uri));
|
||||
else
|
||||
throw Error('Unsupported `contentScriptFile` url: ' + String(uri));
|
||||
}
|
||||
catch(e) {
|
||||
emit(worker, 'error', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setListeners (workerSandbox, console) {
|
||||
let { worker } = modelFor(workerSandbox);
|
||||
// console.xxx calls
|
||||
workerSandbox.on('console', function consoleListener (kind, ...args) {
|
||||
console[kind].apply(console, args);
|
||||
});
|
||||
|
||||
// self.postMessage calls
|
||||
workerSandbox.on('message', function postMessage(data) {
|
||||
// destroyed?
|
||||
if (worker)
|
||||
emit(worker, 'message', data);
|
||||
});
|
||||
|
||||
// self.port.emit calls
|
||||
workerSandbox.on('event', function portEmit (...eventArgs) {
|
||||
// If not destroyed, emit event information to worker
|
||||
// `eventArgs` has the event name as first element,
|
||||
// and remaining elements are additional arguments to pass
|
||||
if (worker)
|
||||
emit.apply(null, [worker.port].concat(eventArgs));
|
||||
});
|
||||
|
||||
// unwrap, recreate and propagate async Errors thrown from content-script
|
||||
workerSandbox.on('error', function onError({instanceOfError, value}) {
|
||||
if (worker) {
|
||||
let error = value;
|
||||
if (instanceOfError) {
|
||||
error = new Error(value.message, value.fileName, value.lineNumber);
|
||||
error.stack = value.stack;
|
||||
error.name = value.name;
|
||||
}
|
||||
emit(worker, 'error', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates code in the sandbox.
|
||||
* @param {String} code
|
||||
* JavaScript source to evaluate.
|
||||
* @param {String} [filename='javascript:' + code]
|
||||
* Name of the file
|
||||
*/
|
||||
function evaluateIn (workerSandbox, code, filename) {
|
||||
let { worker, sandbox } = modelFor(workerSandbox);
|
||||
try {
|
||||
evaluate(sandbox, code, filename || 'javascript:' + code);
|
||||
}
|
||||
catch(e) {
|
||||
emit(worker, 'error', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called by the worker sandbox when it needs to send a message
|
||||
*/
|
||||
function onContentEvent (workerSandbox, args) {
|
||||
// As `emit`, we ensure having an asynchronous behavior
|
||||
async(function () {
|
||||
// We emit event to chrome/addon listeners
|
||||
emit.apply(null, [workerSandbox].concat(JSON.parse(args)));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function modelFor (workerSandbox) {
|
||||
return sandboxes.get(workerSandbox);
|
||||
}
|
||||
|
||||
function getUnsafeWindow (win) {
|
||||
return win.wrappedJSObject || win;
|
||||
}
|
||||
|
||||
function emitToContent (workerSandbox, args) {
|
||||
return modelFor(workerSandbox).emitToContent(args);
|
||||
}
|
||||
|
||||
function createChromeAPI (scope) {
|
||||
return Cu.cloneInto({
|
||||
timers: {
|
||||
setTimeout: timer.setTimeout.bind(timer),
|
||||
setInterval: timer.setInterval.bind(timer),
|
||||
clearTimeout: timer.clearTimeout.bind(timer),
|
||||
clearInterval: timer.clearInterval.bind(timer),
|
||||
},
|
||||
sandbox: {
|
||||
evaluate: evaluate,
|
||||
},
|
||||
}, scope, {cloneFunctions: true});
|
||||
}
|
||||
12
toolkit/jetpack/sdk/content/sandbox/events.js
Normal file
12
toolkit/jetpack/sdk/content/sandbox/events.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const events = {};
|
||||
exports.events = events;
|
||||
58
toolkit/jetpack/sdk/content/tab-events.js
Normal file
58
toolkit/jetpack/sdk/content/tab-events.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/* 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 { Ci } = require('chrome');
|
||||
const system = require('sdk/system/events');
|
||||
const { frames } = require('sdk/remote/child');
|
||||
const { WorkerChild } = require('sdk/content/worker-child');
|
||||
|
||||
// map observer topics to tab event names
|
||||
const EVENTS = {
|
||||
'content-document-global-created': 'create',
|
||||
'chrome-document-global-created': 'create',
|
||||
'content-document-interactive': 'ready',
|
||||
'chrome-document-interactive': 'ready',
|
||||
'content-document-loaded': 'load',
|
||||
'chrome-document-loaded': 'load',
|
||||
// 'content-page-shown': 'pageshow', // bug 1024105
|
||||
}
|
||||
|
||||
function topicListener({ subject, type }) {
|
||||
// NOTE detect the window from the subject:
|
||||
// - on *-global-created the subject is the window
|
||||
// - in the other cases it is the document object
|
||||
let window = subject instanceof Ci.nsIDOMWindow ? subject : subject.defaultView;
|
||||
if (!window){
|
||||
return;
|
||||
}
|
||||
let frame = frames.getFrameForWindow(window);
|
||||
if (frame) {
|
||||
let readyState = frame.content.document.readyState;
|
||||
frame.port.emit('sdk/tab/event', EVENTS[type], { readyState });
|
||||
}
|
||||
}
|
||||
|
||||
for (let topic in EVENTS)
|
||||
system.on(topic, topicListener, true);
|
||||
|
||||
// bug 1024105 - content-page-shown notification doesn't pass persisted param
|
||||
function eventListener({target, type, persisted}) {
|
||||
let frame = this;
|
||||
if (target === frame.content.document) {
|
||||
frame.port.emit('sdk/tab/event', type, persisted);
|
||||
}
|
||||
}
|
||||
frames.addEventListener('pageshow', eventListener, true);
|
||||
|
||||
frames.port.on('sdk/tab/attach', (frame, options) => {
|
||||
options.window = frame.content;
|
||||
new WorkerChild(options);
|
||||
});
|
||||
|
||||
// Forward the existent frames's readyState.
|
||||
for (let frame of frames) {
|
||||
let readyState = frame.content.document.readyState;
|
||||
frame.port.emit('sdk/tab/event', 'init', { readyState });
|
||||
}
|
||||
51
toolkit/jetpack/sdk/content/thumbnail.js
Normal file
51
toolkit/jetpack/sdk/content/thumbnail.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/* 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';
|
||||
|
||||
module.metadata = {
|
||||
'stability': 'unstable'
|
||||
};
|
||||
|
||||
const { Cc, Ci, Cu } = require('chrome');
|
||||
const AppShellService = Cc['@mozilla.org/appshell/appShellService;1'].
|
||||
getService(Ci.nsIAppShellService);
|
||||
|
||||
const NS = 'http://www.w3.org/1999/xhtml';
|
||||
const COLOR = 'rgb(255,255,255)';
|
||||
|
||||
/**
|
||||
* Creates canvas element with a thumbnail of the passed window.
|
||||
* @param {Window} window
|
||||
* @returns {Element}
|
||||
*/
|
||||
function getThumbnailCanvasForWindow(window) {
|
||||
let aspectRatio = 0.5625; // 16:9
|
||||
let thumbnail = AppShellService.hiddenDOMWindow.document
|
||||
.createElementNS(NS, 'canvas');
|
||||
thumbnail.mozOpaque = true;
|
||||
thumbnail.width = Math.ceil(window.screen.availWidth / 5.75);
|
||||
thumbnail.height = Math.round(thumbnail.width * aspectRatio);
|
||||
let ctx = thumbnail.getContext('2d');
|
||||
let snippetWidth = window.innerWidth * .6;
|
||||
let scale = thumbnail.width / snippetWidth;
|
||||
ctx.scale(scale, scale);
|
||||
ctx.drawWindow(window, window.scrollX, window.scrollY, snippetWidth,
|
||||
snippetWidth * aspectRatio, COLOR);
|
||||
return thumbnail;
|
||||
}
|
||||
exports.getThumbnailCanvasForWindow = getThumbnailCanvasForWindow;
|
||||
|
||||
/**
|
||||
* Creates Base64 encoded data URI of the thumbnail for the passed window.
|
||||
* @param {Window} window
|
||||
* @returns {String}
|
||||
*/
|
||||
exports.getThumbnailURIForWindow = function getThumbnailURIForWindow(window) {
|
||||
return getThumbnailCanvasForWindow(window).toDataURL()
|
||||
};
|
||||
|
||||
// default 80x45 blank when not available
|
||||
exports.BLANK = 'data:image/png;base64,' +
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAFAAAAAtCAYAAAA5reyyAAAAJElEQVRoge3BAQ'+
|
||||
'EAAACCIP+vbkhAAQAAAAAAAAAAAAAAAADXBjhtAAGQ0AF/AAAAAElFTkSuQmCC';
|
||||
105
toolkit/jetpack/sdk/content/utils.js
Normal file
105
toolkit/jetpack/sdk/content/utils.js
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
/* 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';
|
||||
|
||||
module.metadata = {
|
||||
'stability': 'unstable'
|
||||
};
|
||||
|
||||
var { merge } = require('../util/object');
|
||||
var { data } = require('../self');
|
||||
var assetsURI = data.url();
|
||||
var isArray = Array.isArray;
|
||||
var method = require('../../method/core');
|
||||
var { uuid } = require('../util/uuid');
|
||||
|
||||
const isAddonContent = ({ contentURL }) =>
|
||||
contentURL && data.url(contentURL).startsWith(assetsURI);
|
||||
|
||||
exports.isAddonContent = isAddonContent;
|
||||
|
||||
function hasContentScript({ contentScript, contentScriptFile }) {
|
||||
return (isArray(contentScript) ? contentScript.length > 0 :
|
||||
!!contentScript) ||
|
||||
(isArray(contentScriptFile) ? contentScriptFile.length > 0 :
|
||||
!!contentScriptFile);
|
||||
}
|
||||
exports.hasContentScript = hasContentScript;
|
||||
|
||||
function requiresAddonGlobal(model) {
|
||||
return model.injectInDocument || (isAddonContent(model) && !hasContentScript(model));
|
||||
}
|
||||
exports.requiresAddonGlobal = requiresAddonGlobal;
|
||||
|
||||
function getAttachEventType(model) {
|
||||
if (!model) return null;
|
||||
let when = model.contentScriptWhen;
|
||||
return requiresAddonGlobal(model) ? 'document-element-inserted' :
|
||||
when === 'start' ? 'document-element-inserted' :
|
||||
when === 'ready' ? 'DOMContentLoaded' :
|
||||
when === 'end' ? 'load' :
|
||||
null;
|
||||
}
|
||||
exports.getAttachEventType = getAttachEventType;
|
||||
|
||||
var attach = method('worker-attach');
|
||||
exports.attach = attach;
|
||||
|
||||
var connect = method('worker-connect');
|
||||
exports.connect = connect;
|
||||
|
||||
var detach = method('worker-detach');
|
||||
exports.detach = detach;
|
||||
|
||||
var destroy = method('worker-destroy');
|
||||
exports.destroy = destroy;
|
||||
|
||||
function WorkerHost (workerFor) {
|
||||
// Define worker properties that just proxy to underlying worker
|
||||
return ['postMessage', 'port', 'url', 'tab'].reduce(function(proto, name) {
|
||||
// Use descriptor properties instead so we can call
|
||||
// the worker function in the context of the worker so we
|
||||
// don't have to create new functions with `fn.bind(worker)`
|
||||
let descriptorProp = {
|
||||
value: function (...args) {
|
||||
let worker = workerFor(this);
|
||||
return worker[name].apply(worker, args);
|
||||
}
|
||||
};
|
||||
|
||||
let accessorProp = {
|
||||
get: function () { return workerFor(this)[name]; },
|
||||
set: function (value) { workerFor(this)[name] = value; }
|
||||
};
|
||||
|
||||
Object.defineProperty(proto, name, merge({
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
}, isDescriptor(name) ? descriptorProp : accessorProp));
|
||||
return proto;
|
||||
}, {});
|
||||
|
||||
function isDescriptor (prop) {
|
||||
return ~['postMessage'].indexOf(prop);
|
||||
}
|
||||
}
|
||||
exports.WorkerHost = WorkerHost;
|
||||
|
||||
function makeChildOptions(options) {
|
||||
function makeStringArray(arrayOrValue) {
|
||||
if (!arrayOrValue)
|
||||
return [];
|
||||
return [].concat(arrayOrValue).map(String);
|
||||
}
|
||||
|
||||
return {
|
||||
id: String(uuid()),
|
||||
contentScript: makeStringArray(options.contentScript),
|
||||
contentScriptFile: makeStringArray(options.contentScriptFile),
|
||||
contentScriptOptions: options.contentScriptOptions ?
|
||||
JSON.stringify(options.contentScriptOptions) :
|
||||
null,
|
||||
}
|
||||
}
|
||||
exports.makeChildOptions = makeChildOptions;
|
||||
158
toolkit/jetpack/sdk/content/worker-child.js
Normal file
158
toolkit/jetpack/sdk/content/worker-child.js
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
/* 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 { merge } = require('../util/object');
|
||||
const { Class } = require('../core/heritage');
|
||||
const { emit } = require('../event/core');
|
||||
const { EventTarget } = require('../event/target');
|
||||
const { getInnerId, getByInnerId } = require('../window/utils');
|
||||
const { instanceOf, isObject } = require('../lang/type');
|
||||
const system = require('../system/events');
|
||||
const { when } = require('../system/unload');
|
||||
const { WorkerSandbox } = require('./sandbox');
|
||||
const { Ci } = require('chrome');
|
||||
const { process, frames } = require('../remote/child');
|
||||
|
||||
const EVENTS = {
|
||||
'chrome-page-shown': 'pageshow',
|
||||
'content-page-shown': 'pageshow',
|
||||
'chrome-page-hidden': 'pagehide',
|
||||
'content-page-hidden': 'pagehide',
|
||||
'inner-window-destroyed': 'detach',
|
||||
}
|
||||
|
||||
// The parent Worker must have been created (or an async message sent to spawn
|
||||
// its creation) before creating the WorkerChild or messages from the content
|
||||
// script to the parent will get lost.
|
||||
const WorkerChild = Class({
|
||||
implements: [EventTarget],
|
||||
|
||||
initialize(options) {
|
||||
merge(this, options);
|
||||
keepAlive.set(this.id, this);
|
||||
|
||||
this.windowId = getInnerId(this.window);
|
||||
if (this.contentScriptOptions)
|
||||
this.contentScriptOptions = JSON.parse(this.contentScriptOptions);
|
||||
|
||||
this.port = EventTarget();
|
||||
this.port.on('*', this.send.bind(this, 'event'));
|
||||
this.on('*', this.send.bind(this));
|
||||
|
||||
this.observe = this.observe.bind(this);
|
||||
|
||||
for (let topic in EVENTS)
|
||||
system.on(topic, this.observe);
|
||||
|
||||
this.receive = this.receive.bind(this);
|
||||
process.port.on('sdk/worker/message', this.receive);
|
||||
|
||||
this.sandbox = WorkerSandbox(this, this.window);
|
||||
|
||||
// If the document has an unexpected readyState, its worker-child instance is initialized
|
||||
// as frozen until one of the known readyState is reached.
|
||||
let initialDocumentReadyState = this.window.document.readyState;
|
||||
this.frozen = [
|
||||
"loading", "interactive", "complete"
|
||||
].includes(initialDocumentReadyState) ? false : true;
|
||||
|
||||
if (this.frozen) {
|
||||
console.warn("SDK worker-child started as frozen on unexpected initial document.readyState", {
|
||||
initialDocumentReadyState, windowLocation: this.window.location.href,
|
||||
});
|
||||
}
|
||||
|
||||
this.frozenMessages = [];
|
||||
this.on('pageshow', () => {
|
||||
this.frozen = false;
|
||||
this.frozenMessages.forEach(args => this.sandbox.emit(...args));
|
||||
this.frozenMessages = [];
|
||||
});
|
||||
this.on('pagehide', () => {
|
||||
this.frozen = true;
|
||||
});
|
||||
},
|
||||
|
||||
// messages
|
||||
receive(process, id, args) {
|
||||
if (id !== this.id)
|
||||
return;
|
||||
args = JSON.parse(args);
|
||||
|
||||
if (this.frozen)
|
||||
this.frozenMessages.push(args);
|
||||
else
|
||||
this.sandbox.emit(...args);
|
||||
|
||||
if (args[0] === 'detach')
|
||||
this.destroy(args[1]);
|
||||
},
|
||||
|
||||
send(...args) {
|
||||
process.port.emit('sdk/worker/event', this.id, JSON.stringify(args, exceptions));
|
||||
},
|
||||
|
||||
// notifications
|
||||
observe({ type, subject }) {
|
||||
if (!this.sandbox)
|
||||
return;
|
||||
|
||||
if (subject.defaultView && getInnerId(subject.defaultView) === this.windowId) {
|
||||
this.sandbox.emitSync(EVENTS[type]);
|
||||
emit(this, EVENTS[type]);
|
||||
}
|
||||
|
||||
if (type === 'inner-window-destroyed' &&
|
||||
subject.QueryInterface(Ci.nsISupportsPRUint64).data === this.windowId) {
|
||||
this.destroy();
|
||||
}
|
||||
},
|
||||
|
||||
get frame() {
|
||||
return frames.getFrameForWindow(this.window.top);
|
||||
},
|
||||
|
||||
// detach/destroy: unload and release the sandbox
|
||||
destroy(reason) {
|
||||
if (!this.sandbox)
|
||||
return;
|
||||
|
||||
for (let topic in EVENTS)
|
||||
system.off(topic, this.observe);
|
||||
process.port.off('sdk/worker/message', this.receive);
|
||||
|
||||
this.sandbox.destroy(reason);
|
||||
this.sandbox = null;
|
||||
keepAlive.delete(this.id);
|
||||
|
||||
this.send('detach');
|
||||
}
|
||||
})
|
||||
exports.WorkerChild = WorkerChild;
|
||||
|
||||
// Error instances JSON poorly
|
||||
function exceptions(key, value) {
|
||||
if (!isObject(value) || !instanceOf(value, Error))
|
||||
return value;
|
||||
let _errorType = value.constructor.name;
|
||||
let { message, fileName, lineNumber, stack, name } = value;
|
||||
return { _errorType, message, fileName, lineNumber, stack, name };
|
||||
}
|
||||
|
||||
// workers for windows in this tab
|
||||
var keepAlive = new Map();
|
||||
|
||||
process.port.on('sdk/worker/create', (process, options, cpows) => {
|
||||
options.window = cpows.window;
|
||||
let worker = new WorkerChild(options);
|
||||
|
||||
let frame = frames.getFrameForWindow(options.window.top);
|
||||
frame.port.emit('sdk/worker/connect', options.id, options.window.location.href);
|
||||
});
|
||||
|
||||
when(reason => {
|
||||
for (let worker of keepAlive.values())
|
||||
worker.destroy(reason);
|
||||
});
|
||||
180
toolkit/jetpack/sdk/content/worker.js
Normal file
180
toolkit/jetpack/sdk/content/worker.js
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { emit } = require('../event/core');
|
||||
const { omit, merge } = require('../util/object');
|
||||
const { Class } = require('../core/heritage');
|
||||
const { method } = require('../lang/functional');
|
||||
const { getInnerId } = require('../window/utils');
|
||||
const { EventTarget } = require('../event/target');
|
||||
const { isPrivate } = require('../private-browsing/utils');
|
||||
const { getTabForBrowser, getTabForContentWindowNoShim, getBrowserForTab } = require('../tabs/utils');
|
||||
const { attach, connect, detach, destroy, makeChildOptions } = require('./utils');
|
||||
const { ensure } = require('../system/unload');
|
||||
const { on: observe } = require('../system/events');
|
||||
const { Ci, Cu } = require('chrome');
|
||||
const { modelFor: tabFor } = require('sdk/model/core');
|
||||
const { remoteRequire, processes, frames } = require('../remote/parent');
|
||||
remoteRequire('sdk/content/worker-child');
|
||||
|
||||
const workers = new WeakMap();
|
||||
var modelFor = (worker) => workers.get(worker);
|
||||
|
||||
const ERR_DESTROYED = "Couldn't find the worker to receive this message. " +
|
||||
"The script may not be initialized yet, or may already have been unloaded.";
|
||||
|
||||
// a handle for communication between content script and addon code
|
||||
const Worker = Class({
|
||||
implements: [EventTarget],
|
||||
|
||||
initialize(options = {}) {
|
||||
ensure(this, 'detach');
|
||||
|
||||
let model = {
|
||||
attached: false,
|
||||
destroyed: false,
|
||||
earlyEvents: [], // fired before worker was attached
|
||||
frozen: true, // document is not yet active
|
||||
options,
|
||||
};
|
||||
workers.set(this, model);
|
||||
|
||||
this.on('detach', this.detach);
|
||||
EventTarget.prototype.initialize.call(this, options);
|
||||
|
||||
this.receive = this.receive.bind(this);
|
||||
|
||||
this.port = EventTarget();
|
||||
this.port.emit = this.send.bind(this, 'event');
|
||||
this.postMessage = this.send.bind(this, 'message');
|
||||
|
||||
if ('window' in options) {
|
||||
let window = options.window;
|
||||
delete options.window;
|
||||
attach(this, window);
|
||||
}
|
||||
},
|
||||
|
||||
// messages
|
||||
receive(process, id, args) {
|
||||
let model = modelFor(this);
|
||||
if (id !== model.id || !model.attached)
|
||||
return;
|
||||
args = JSON.parse(args);
|
||||
if (model.destroyed && args[0] != 'detach')
|
||||
return;
|
||||
|
||||
if (args[0] === 'event')
|
||||
emit(this.port, ...args.slice(1))
|
||||
else
|
||||
emit(this, ...args);
|
||||
},
|
||||
|
||||
send(...args) {
|
||||
let model = modelFor(this);
|
||||
if (model.destroyed && args[0] !== 'detach')
|
||||
throw new Error(ERR_DESTROYED);
|
||||
|
||||
if (!model.attached) {
|
||||
model.earlyEvents.push(args);
|
||||
return;
|
||||
}
|
||||
|
||||
processes.port.emit('sdk/worker/message', model.id, JSON.stringify(args));
|
||||
},
|
||||
|
||||
// properties
|
||||
get url() {
|
||||
let { url } = modelFor(this);
|
||||
return url;
|
||||
},
|
||||
|
||||
get contentURL() {
|
||||
return this.url;
|
||||
},
|
||||
|
||||
get tab() {
|
||||
require('sdk/tabs');
|
||||
let { frame } = modelFor(this);
|
||||
if (!frame)
|
||||
return null;
|
||||
let rawTab = getTabForBrowser(frame.frameElement);
|
||||
return rawTab && tabFor(rawTab);
|
||||
},
|
||||
|
||||
toString: () => '[object Worker]',
|
||||
|
||||
detach: method(detach),
|
||||
destroy: method(destroy),
|
||||
})
|
||||
exports.Worker = Worker;
|
||||
|
||||
attach.define(Worker, function(worker, window) {
|
||||
let model = modelFor(worker);
|
||||
if (model.attached)
|
||||
detach(worker);
|
||||
|
||||
let childOptions = makeChildOptions(model.options);
|
||||
processes.port.emitCPOW('sdk/worker/create', [childOptions], { window });
|
||||
|
||||
let listener = (frame, id, url) => {
|
||||
if (id != childOptions.id)
|
||||
return;
|
||||
frames.port.off('sdk/worker/connect', listener);
|
||||
connect(worker, frame, { id, url });
|
||||
};
|
||||
frames.port.on('sdk/worker/connect', listener);
|
||||
});
|
||||
|
||||
connect.define(Worker, function(worker, frame, { id, url }) {
|
||||
let model = modelFor(worker);
|
||||
if (model.attached)
|
||||
detach(worker);
|
||||
|
||||
model.id = id;
|
||||
model.frame = frame;
|
||||
model.url = url;
|
||||
|
||||
// Messages from content -> chrome come through the process message manager
|
||||
// since that lives longer than the frame message manager
|
||||
processes.port.on('sdk/worker/event', worker.receive);
|
||||
|
||||
model.attached = true;
|
||||
model.destroyed = false;
|
||||
model.frozen = false;
|
||||
|
||||
model.earlyEvents.forEach(args => worker.send(...args));
|
||||
model.earlyEvents = [];
|
||||
emit(worker, 'attach');
|
||||
});
|
||||
|
||||
// unload and release the child worker, release window reference
|
||||
detach.define(Worker, function(worker) {
|
||||
let model = modelFor(worker);
|
||||
if (!model.attached)
|
||||
return;
|
||||
|
||||
processes.port.off('sdk/worker/event', worker.receive);
|
||||
model.attached = false;
|
||||
model.destroyed = true;
|
||||
emit(worker, 'detach');
|
||||
});
|
||||
|
||||
isPrivate.define(Worker, ({ tab }) => isPrivate(tab));
|
||||
|
||||
// Something in the parent side has destroyed the worker, tell the child to
|
||||
// detach, the child will respond when it has detached
|
||||
destroy.define(Worker, function(worker, reason) {
|
||||
let model = modelFor(worker);
|
||||
model.destroyed = true;
|
||||
if (!model.attached)
|
||||
return;
|
||||
|
||||
worker.send('detach', reason);
|
||||
});
|
||||
1188
toolkit/jetpack/sdk/context-menu.js
Normal file
1188
toolkit/jetpack/sdk/context-menu.js
Normal file
File diff suppressed because it is too large
Load diff
147
toolkit/jetpack/sdk/context-menu/context.js
Normal file
147
toolkit/jetpack/sdk/context-menu/context.js
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
/* 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 { Class } = require("../core/heritage");
|
||||
const { extend } = require("../util/object");
|
||||
const { MatchPattern } = require("../util/match-pattern");
|
||||
const readers = require("./readers");
|
||||
|
||||
// Context class is required to implement a single `isCurrent(target)` method
|
||||
// that must return boolean value indicating weather given target matches a
|
||||
// context or not. Most context implementations below will have an associated
|
||||
// reader that way context implementation can setup a reader to extract necessary
|
||||
// information to make decision if target is matching a context.
|
||||
const Context = Class({
|
||||
isRequired: false,
|
||||
isCurrent(target) {
|
||||
throw Error("Context class must implement isCurrent(target) method");
|
||||
},
|
||||
get required() {
|
||||
Object.defineProperty(this, "required", {
|
||||
value: Object.assign(Object.create(Object.getPrototypeOf(this)),
|
||||
this,
|
||||
{isRequired: true})
|
||||
});
|
||||
return this.required;
|
||||
}
|
||||
});
|
||||
Context.required = function(...params) {
|
||||
return Object.assign(new this(...params), {isRequired: true});
|
||||
};
|
||||
exports.Context = Context;
|
||||
|
||||
|
||||
// Next few context implementations use an associated reader to extract info
|
||||
// from the context target and story it to a private symbol associtaed with
|
||||
// a context implementation. That way name collisions are avoided while required
|
||||
// information is still carried along.
|
||||
const isPage = Symbol("context/page?")
|
||||
const PageContext = Class({
|
||||
extends: Context,
|
||||
read: {[isPage]: new readers.isPage()},
|
||||
isCurrent: target => target[isPage]
|
||||
});
|
||||
exports.Page = PageContext;
|
||||
|
||||
const isFrame = Symbol("context/frame?");
|
||||
const FrameContext = Class({
|
||||
extends: Context,
|
||||
read: {[isFrame]: new readers.isFrame()},
|
||||
isCurrent: target => target[isFrame]
|
||||
});
|
||||
exports.Frame = FrameContext;
|
||||
|
||||
const selection = Symbol("context/selection")
|
||||
const SelectionContext = Class({
|
||||
read: {[selection]: new readers.Selection()},
|
||||
isCurrent: target => !!target[selection]
|
||||
});
|
||||
exports.Selection = SelectionContext;
|
||||
|
||||
const link = Symbol("context/link");
|
||||
const LinkContext = Class({
|
||||
extends: Context,
|
||||
read: {[link]: new readers.LinkURL()},
|
||||
isCurrent: target => !!target[link]
|
||||
});
|
||||
exports.Link = LinkContext;
|
||||
|
||||
const isEditable = Symbol("context/editable?")
|
||||
const EditableContext = Class({
|
||||
extends: Context,
|
||||
read: {[isEditable]: new readers.isEditable()},
|
||||
isCurrent: target => target[isEditable]
|
||||
});
|
||||
exports.Editable = EditableContext;
|
||||
|
||||
|
||||
const mediaType = Symbol("context/mediaType")
|
||||
|
||||
const ImageContext = Class({
|
||||
extends: Context,
|
||||
read: {[mediaType]: new readers.MediaType()},
|
||||
isCurrent: target => target[mediaType] === "image"
|
||||
});
|
||||
exports.Image = ImageContext;
|
||||
|
||||
|
||||
const VideoContext = Class({
|
||||
extends: Context,
|
||||
read: {[mediaType]: new readers.MediaType()},
|
||||
isCurrent: target => target[mediaType] === "video"
|
||||
});
|
||||
exports.Video = VideoContext;
|
||||
|
||||
|
||||
const AudioContext = Class({
|
||||
extends: Context,
|
||||
read: {[mediaType]: new readers.MediaType()},
|
||||
isCurrent: target => target[mediaType] === "audio"
|
||||
});
|
||||
exports.Audio = AudioContext;
|
||||
|
||||
const isSelectorMatch = Symbol("context/selector/mathches?")
|
||||
const SelectorContext = Class({
|
||||
extends: Context,
|
||||
initialize(selector) {
|
||||
this.selector = selector;
|
||||
// Each instance of selector context will need to store read
|
||||
// data into different field, so that case with multilpe selector
|
||||
// contexts won't cause a conflicts.
|
||||
this[isSelectorMatch] = Symbol(selector);
|
||||
this.read = {[this[isSelectorMatch]]: new readers.SelectorMatch(selector)};
|
||||
},
|
||||
isCurrent(target) {
|
||||
return target[this[isSelectorMatch]];
|
||||
}
|
||||
});
|
||||
exports.Selector = SelectorContext;
|
||||
|
||||
const url = Symbol("context/url");
|
||||
const URLContext = Class({
|
||||
extends: Context,
|
||||
initialize(pattern) {
|
||||
this.pattern = new MatchPattern(pattern);
|
||||
},
|
||||
read: {[url]: new readers.PageURL()},
|
||||
isCurrent(target) {
|
||||
return this.pattern.test(target[url]);
|
||||
}
|
||||
});
|
||||
exports.URL = URLContext;
|
||||
|
||||
var PredicateContext = Class({
|
||||
extends: Context,
|
||||
initialize(isMatch) {
|
||||
if (typeof(isMatch) !== "function") {
|
||||
throw TypeError("Predicate context mus be passed a function");
|
||||
}
|
||||
|
||||
this.isMatch = isMatch
|
||||
},
|
||||
isCurrent(target) {
|
||||
return this.isMatch(target);
|
||||
}
|
||||
});
|
||||
exports.Predicate = PredicateContext;
|
||||
384
toolkit/jetpack/sdk/context-menu/core.js
Normal file
384
toolkit/jetpack/sdk/context-menu/core.js
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
/* 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 Contexts = require("./context");
|
||||
const Readers = require("./readers");
|
||||
const Component = require("../ui/component");
|
||||
const { Class } = require("../core/heritage");
|
||||
const { map, filter, object, reduce, keys, symbols,
|
||||
pairs, values, each, some, isEvery, count } = require("../util/sequence");
|
||||
const { loadModule } = require("framescript/manager");
|
||||
const { Cu, Cc, Ci } = require("chrome");
|
||||
const prefs = require("sdk/preferences/service");
|
||||
|
||||
const globalMessageManager = Cc["@mozilla.org/globalmessagemanager;1"]
|
||||
.getService(Ci.nsIMessageListenerManager);
|
||||
const preferencesService = Cc["@mozilla.org/preferences-service;1"].
|
||||
getService(Ci.nsIPrefService).
|
||||
getBranch(null);
|
||||
|
||||
|
||||
const readTable = Symbol("context-menu/read-table");
|
||||
const nameTable = Symbol("context-menu/name-table");
|
||||
const onContext = Symbol("context-menu/on-context");
|
||||
const isMatching = Symbol("context-menu/matching-handler?");
|
||||
|
||||
exports.onContext = onContext;
|
||||
exports.readTable = readTable;
|
||||
exports.nameTable = nameTable;
|
||||
|
||||
|
||||
const propagateOnContext = (item, data) =>
|
||||
each(child => child[onContext](data), item.state.children);
|
||||
|
||||
const isContextMatch = item => !item[isMatching] || item[isMatching]();
|
||||
|
||||
// For whatever reason addWeakMessageListener does not seems to work as our
|
||||
// instance seems to dropped even though it's alive. This is simple workaround
|
||||
// to avoid dead object excetptions.
|
||||
const WeakMessageListener = function(receiver, handler="receiveMessage") {
|
||||
this.receiver = receiver
|
||||
this.handler = handler
|
||||
};
|
||||
WeakMessageListener.prototype = {
|
||||
constructor: WeakMessageListener,
|
||||
receiveMessage(message) {
|
||||
if (Cu.isDeadWrapper(this.receiver)) {
|
||||
message.target.messageManager.removeMessageListener(message.name, this);
|
||||
}
|
||||
else {
|
||||
this.receiver[this.handler](message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const OVERFLOW_THRESH = "extensions.addon-sdk.context-menu.overflowThreshold";
|
||||
const onMessage = Symbol("context-menu/message-listener");
|
||||
const onPreferceChange = Symbol("context-menu/preference-change");
|
||||
const ContextMenuExtension = Class({
|
||||
extends: Component,
|
||||
initialize: Component,
|
||||
setup() {
|
||||
const messageListener = new WeakMessageListener(this, onMessage);
|
||||
loadModule(globalMessageManager, "framescript/context-menu", true, "onContentFrame");
|
||||
globalMessageManager.addMessageListener("sdk/context-menu/read", messageListener);
|
||||
globalMessageManager.addMessageListener("sdk/context-menu/readers?", messageListener);
|
||||
|
||||
preferencesService.addObserver(OVERFLOW_THRESH, this, false);
|
||||
},
|
||||
observe(_, __, name) {
|
||||
if (name === OVERFLOW_THRESH) {
|
||||
const overflowThreshold = prefs.get(OVERFLOW_THRESH, 10);
|
||||
this[Component.patch]({overflowThreshold});
|
||||
}
|
||||
},
|
||||
[onMessage]({name, data, target}) {
|
||||
if (name === "sdk/context-menu/read")
|
||||
this[onContext]({target, data});
|
||||
if (name === "sdk/context-menu/readers?")
|
||||
target.messageManager.sendAsyncMessage("sdk/context-menu/readers",
|
||||
JSON.parse(JSON.stringify(this.state.readers)));
|
||||
},
|
||||
[Component.initial](options={}, children) {
|
||||
const element = options.element || null;
|
||||
const target = options.target || null;
|
||||
const readers = Object.create(null);
|
||||
const users = Object.create(null);
|
||||
const registry = new WeakSet();
|
||||
const overflowThreshold = prefs.get(OVERFLOW_THRESH, 10);
|
||||
|
||||
return { target, children: [], readers, users, element,
|
||||
registry, overflowThreshold };
|
||||
},
|
||||
[Component.isUpdated](before, after) {
|
||||
// Update only if target changed, since there is no point in re-rendering
|
||||
// when children are. Also new items added won't be in sync with a latest
|
||||
// context target so we should really just render before drawing context
|
||||
// menu.
|
||||
return before.target !== after.target;
|
||||
},
|
||||
[Component.render]({element, children, overflowThreshold}) {
|
||||
if (!element) return null;
|
||||
|
||||
const items = children.filter(isContextMatch);
|
||||
const body = items.length === 0 ? items :
|
||||
items.length < overflowThreshold ? [new Separator(),
|
||||
...items] :
|
||||
[{tagName: "menu",
|
||||
className: "sdk-context-menu-overflow-menu",
|
||||
label: "Add-ons",
|
||||
accesskey: "A",
|
||||
children: [{tagName: "menupopup",
|
||||
children: items}]}];
|
||||
return {
|
||||
element: element,
|
||||
tagName: "menugroup",
|
||||
style: "-moz-box-orient: vertical;",
|
||||
className: "sdk-context-menu-extension",
|
||||
children: body
|
||||
}
|
||||
},
|
||||
// Adds / remove child to it's own list.
|
||||
add(item) {
|
||||
this[Component.patch]({children: this.state.children.concat(item)});
|
||||
},
|
||||
remove(item) {
|
||||
this[Component.patch]({
|
||||
children: this.state.children.filter(x => x !== item)
|
||||
});
|
||||
},
|
||||
register(item) {
|
||||
const { users, registry } = this.state;
|
||||
if (registry.has(item)) return;
|
||||
registry.add(item);
|
||||
|
||||
// Each (ContextHandler) item has a readTable that is a
|
||||
// map of keys to readers extracting them from the content.
|
||||
// During the registraction we update intrnal record of unique
|
||||
// readers and users per reader. Most context will have a reader
|
||||
// shared across all instances there for map of users per reader
|
||||
// is stored separately from the reader so that removing reader
|
||||
// will occur only when no users remain.
|
||||
const table = item[readTable];
|
||||
// Context readers store data in private symbols so we need to
|
||||
// collect both table keys and private symbols.
|
||||
const names = [...keys(table), ...symbols(table)];
|
||||
const readers = map(name => table[name], names);
|
||||
// Create delta for registered readers that will be merged into
|
||||
// internal readers table.
|
||||
const added = filter(x => !users[x.id], readers);
|
||||
const delta = object(...map(x => [x.id, x], added));
|
||||
|
||||
const update = reduce((update, reader) => {
|
||||
const n = update[reader.id] || 0;
|
||||
update[reader.id] = n + 1;
|
||||
return update;
|
||||
}, Object.assign({}, users), readers);
|
||||
|
||||
// Patch current state with a changes that registered item caused.
|
||||
this[Component.patch]({users: update,
|
||||
readers: Object.assign(this.state.readers, delta)});
|
||||
|
||||
if (count(added)) {
|
||||
globalMessageManager.broadcastAsyncMessage("sdk/context-menu/readers",
|
||||
JSON.parse(JSON.stringify(delta)));
|
||||
}
|
||||
},
|
||||
unregister(item) {
|
||||
const { users, registry } = this.state;
|
||||
if (!registry.has(item)) return;
|
||||
registry.delete(item);
|
||||
|
||||
const table = item[readTable];
|
||||
const names = [...keys(table), ...symbols(table)];
|
||||
const readers = map(name => table[name], names);
|
||||
const update = reduce((update, reader) => {
|
||||
update[reader.id] = update[reader.id] - 1;
|
||||
return update;
|
||||
}, Object.assign({}, users), readers);
|
||||
const removed = filter(id => !update[id], keys(update));
|
||||
const delta = object(...map(x => [x, null], removed));
|
||||
|
||||
this[Component.patch]({users: update,
|
||||
readers: Object.assign(this.state.readers, delta)});
|
||||
|
||||
if (count(removed)) {
|
||||
globalMessageManager.broadcastAsyncMessage("sdk/context-menu/readers",
|
||||
JSON.parse(JSON.stringify(delta)));
|
||||
}
|
||||
},
|
||||
|
||||
[onContext]({data, target}) {
|
||||
propagateOnContext(this, data);
|
||||
const document = target.ownerDocument;
|
||||
const element = document.getElementById("contentAreaContextMenu");
|
||||
|
||||
this[Component.patch]({target: data, element: element});
|
||||
}
|
||||
});this,
|
||||
exports.ContextMenuExtension = ContextMenuExtension;
|
||||
|
||||
// Takes an item options and
|
||||
const makeReadTable = ({context, read}) => {
|
||||
// Result of this function is a tuple of all readers &
|
||||
// name, reader id pairs.
|
||||
|
||||
// Filter down to contexts that have a reader associated.
|
||||
const contexts = filter(context => context.read, context);
|
||||
// Merge all contexts read maps to a single hash, note that there should be
|
||||
// no name collisions as context implementations expect to use private
|
||||
// symbols for storing it's read data.
|
||||
return Object.assign({}, ...map(({read}) => read, contexts), read);
|
||||
}
|
||||
|
||||
const readTarget = (nameTable, data) =>
|
||||
object(...map(([name, id]) => [name, data[id]], nameTable))
|
||||
|
||||
const ContextHandler = Class({
|
||||
extends: Component,
|
||||
initialize: Component,
|
||||
get context() {
|
||||
return this.state.options.context;
|
||||
},
|
||||
get read() {
|
||||
return this.state.options.read;
|
||||
},
|
||||
[Component.initial](options) {
|
||||
return {
|
||||
table: makeReadTable(options),
|
||||
requiredContext: filter(context => context.isRequired, options.context),
|
||||
optionalContext: filter(context => !context.isRequired, options.context)
|
||||
}
|
||||
},
|
||||
[isMatching]() {
|
||||
const {target, requiredContext, optionalContext} = this.state;
|
||||
return isEvery(context => context.isCurrent(target), requiredContext) &&
|
||||
(count(optionalContext) === 0 ||
|
||||
some(context => context.isCurrent(target), optionalContext));
|
||||
},
|
||||
setup() {
|
||||
const table = makeReadTable(this.state.options);
|
||||
this[readTable] = table;
|
||||
this[nameTable] = [...map(symbol => [symbol, table[symbol].id], symbols(table)),
|
||||
...map(name => [name, table[name].id], keys(table))];
|
||||
|
||||
|
||||
contextMenu.register(this);
|
||||
|
||||
each(child => contextMenu.remove(child), this.state.children);
|
||||
contextMenu.add(this);
|
||||
},
|
||||
dispose() {
|
||||
contextMenu.remove(this);
|
||||
|
||||
each(child => contextMenu.unregister(child), this.state.children);
|
||||
contextMenu.unregister(this);
|
||||
},
|
||||
// Internal `Symbol("onContext")` method is invoked when "contextmenu" event
|
||||
// occurs in content process. Context handles with children delegate to each
|
||||
// child and patch it's internal state to reflect new contextmenu target.
|
||||
[onContext](data) {
|
||||
propagateOnContext(this, data);
|
||||
this[Component.patch]({target: readTarget(this[nameTable], data)});
|
||||
}
|
||||
});
|
||||
const isContextHandler = item => item instanceof ContextHandler;
|
||||
|
||||
exports.ContextHandler = ContextHandler;
|
||||
|
||||
const Menu = Class({
|
||||
extends: ContextHandler,
|
||||
[isMatching]() {
|
||||
return ContextHandler.prototype[isMatching].call(this) &&
|
||||
this.state.children.filter(isContextHandler)
|
||||
.some(isContextMatch);
|
||||
},
|
||||
[Component.render]({children, options}) {
|
||||
const items = children.filter(isContextMatch);
|
||||
return {tagName: "menu",
|
||||
className: "sdk-context-menu menu-iconic",
|
||||
label: options.label,
|
||||
accesskey: options.accesskey,
|
||||
image: options.icon,
|
||||
children: [{tagName: "menupopup",
|
||||
children: items}]};
|
||||
}
|
||||
});
|
||||
exports.Menu = Menu;
|
||||
|
||||
const onCommand = Symbol("context-menu/item/onCommand");
|
||||
const Item = Class({
|
||||
extends: ContextHandler,
|
||||
get onClick() {
|
||||
return this.state.options.onClick;
|
||||
},
|
||||
[Component.render]({options}) {
|
||||
const {label, icon, accesskey} = options;
|
||||
return {tagName: "menuitem",
|
||||
className: "sdk-context-menu-item menuitem-iconic",
|
||||
label,
|
||||
accesskey,
|
||||
image: icon,
|
||||
oncommand: this};
|
||||
},
|
||||
handleEvent(event) {
|
||||
if (this.onClick)
|
||||
this.onClick(this.state.target);
|
||||
}
|
||||
});
|
||||
exports.Item = Item;
|
||||
|
||||
var Separator = Class({
|
||||
extends: Component,
|
||||
initialize: Component,
|
||||
[Component.render]() {
|
||||
return {tagName: "menuseparator",
|
||||
className: "sdk-context-menu-separator"}
|
||||
},
|
||||
[onContext]() {
|
||||
|
||||
}
|
||||
});
|
||||
exports.Separator = Separator;
|
||||
|
||||
exports.Contexts = Contexts;
|
||||
exports.Readers = Readers;
|
||||
|
||||
const createElement = (vnode, {document}) => {
|
||||
const node = vnode.namespace ?
|
||||
document.createElementNS(vnode.namespace, vnode.tagName) :
|
||||
document.createElement(vnode.tagName);
|
||||
|
||||
node.setAttribute("data-component-path", vnode[Component.path]);
|
||||
|
||||
each(([key, value]) => {
|
||||
if (key === "tagName") {
|
||||
return;
|
||||
}
|
||||
if (key === "children") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.startsWith("on")) {
|
||||
node.addEventListener(key.substr(2), value)
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof(value) !== "object" &&
|
||||
typeof(value) !== "function" &&
|
||||
value !== void(0) &&
|
||||
value !== null)
|
||||
{
|
||||
if (key === "className") {
|
||||
node[key] = value;
|
||||
}
|
||||
else {
|
||||
node.setAttribute(key, value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}, pairs(vnode));
|
||||
|
||||
each(child => node.appendChild(createElement(child, {document})), vnode.children);
|
||||
return node;
|
||||
};
|
||||
|
||||
const htmlWriter = tree => {
|
||||
if (tree !== null) {
|
||||
const root = tree.element;
|
||||
const node = createElement(tree, {document: root.ownerDocument});
|
||||
const before = root.querySelector("[data-component-path='/']");
|
||||
if (before) {
|
||||
root.replaceChild(node, before);
|
||||
} else {
|
||||
root.appendChild(node);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const contextMenu = ContextMenuExtension();
|
||||
exports.contextMenu = contextMenu;
|
||||
Component.mount(contextMenu, htmlWriter);
|
||||
112
toolkit/jetpack/sdk/context-menu/readers.js
Normal file
112
toolkit/jetpack/sdk/context-menu/readers.js
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/* 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 { Class } = require("../core/heritage");
|
||||
const { extend } = require("../util/object");
|
||||
const { memoize, method, identity } = require("../lang/functional");
|
||||
|
||||
const serializeCategory = ({type}) => ({ category: `reader/${type}()` });
|
||||
|
||||
const Reader = Class({
|
||||
initialize() {
|
||||
this.id = `reader/${this.type}()`
|
||||
},
|
||||
toJSON() {
|
||||
return serializeCategory(this);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const MediaTypeReader = Class({ extends: Reader, type: "MediaType" });
|
||||
exports.MediaType = MediaTypeReader;
|
||||
|
||||
const LinkURLReader = Class({ extends: Reader, type: "LinkURL" });
|
||||
exports.LinkURL = LinkURLReader;
|
||||
|
||||
const SelectionReader = Class({ extends: Reader, type: "Selection" });
|
||||
exports.Selection = SelectionReader;
|
||||
|
||||
const isPageReader = Class({ extends: Reader, type: "isPage" });
|
||||
exports.isPage = isPageReader;
|
||||
|
||||
const isFrameReader = Class({ extends: Reader, type: "isFrame" });
|
||||
exports.isFrame = isFrameReader;
|
||||
|
||||
const isEditable = Class({ extends: Reader, type: "isEditable"});
|
||||
exports.isEditable = isEditable;
|
||||
|
||||
|
||||
|
||||
const ParameterizedReader = Class({
|
||||
extends: Reader,
|
||||
readParameter: function(value) {
|
||||
return value;
|
||||
},
|
||||
toJSON: function() {
|
||||
var json = serializeCategory(this);
|
||||
json[this.parameter] = this[this.parameter];
|
||||
return json;
|
||||
},
|
||||
initialize(...params) {
|
||||
if (params.length) {
|
||||
this[this.parameter] = this.readParameter(...params);
|
||||
}
|
||||
this.id = `reader/${this.type}(${JSON.stringify(this[this.parameter])})`;
|
||||
}
|
||||
});
|
||||
exports.ParameterizedReader = ParameterizedReader;
|
||||
|
||||
|
||||
const QueryReader = Class({
|
||||
extends: ParameterizedReader,
|
||||
type: "Query",
|
||||
parameter: "path"
|
||||
});
|
||||
exports.Query = QueryReader;
|
||||
|
||||
|
||||
const AttributeReader = Class({
|
||||
extends: ParameterizedReader,
|
||||
type: "Attribute",
|
||||
parameter: "name"
|
||||
});
|
||||
exports.Attribute = AttributeReader;
|
||||
|
||||
const SrcURLReader = Class({
|
||||
extends: AttributeReader,
|
||||
name: "src",
|
||||
});
|
||||
exports.SrcURL = SrcURLReader;
|
||||
|
||||
const PageURLReader = Class({
|
||||
extends: QueryReader,
|
||||
path: "ownerDocument.URL",
|
||||
});
|
||||
exports.PageURL = PageURLReader;
|
||||
|
||||
const SelectorMatchReader = Class({
|
||||
extends: ParameterizedReader,
|
||||
type: "SelectorMatch",
|
||||
parameter: "selector"
|
||||
});
|
||||
exports.SelectorMatch = SelectorMatchReader;
|
||||
|
||||
const extractors = new WeakMap();
|
||||
extractors.id = 0;
|
||||
|
||||
|
||||
var Extractor = Class({
|
||||
extends: ParameterizedReader,
|
||||
type: "Extractor",
|
||||
parameter: "source",
|
||||
initialize: function(f) {
|
||||
this[this.parameter] = String(f);
|
||||
if (!extractors.has(f)) {
|
||||
extractors.id = extractors.id + 1;
|
||||
extractors.set(f, extractors.id);
|
||||
}
|
||||
|
||||
this.id = `reader/${this.type}.for(${extractors.get(f)})`
|
||||
}
|
||||
});
|
||||
exports.Extractor = Extractor;
|
||||
32
toolkit/jetpack/sdk/context-menu@2.js
Normal file
32
toolkit/jetpack/sdk/context-menu@2.js
Normal file
|
|
@ -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/. */
|
||||
"use strict";
|
||||
|
||||
const shared = require("toolkit/require");
|
||||
const { Item, Separator, Menu, Contexts, Readers } = shared.require("sdk/context-menu/core");
|
||||
const { setupDisposable, disposeDisposable, Disposable } = require("sdk/core/disposable")
|
||||
const { Class } = require("sdk/core/heritage")
|
||||
|
||||
const makeDisposable = Type => Class({
|
||||
extends: Type,
|
||||
implements: [Disposable],
|
||||
initialize: Type.prototype.initialize,
|
||||
setup(...params) {
|
||||
Type.prototype.setup.call(this, ...params);
|
||||
setupDisposable(this);
|
||||
},
|
||||
dispose(...params) {
|
||||
disposeDisposable(this);
|
||||
Type.prototype.dispose.call(this, ...params);
|
||||
}
|
||||
});
|
||||
|
||||
exports.Separator = Separator;
|
||||
exports.Contexts = Contexts;
|
||||
exports.Readers = Readers;
|
||||
|
||||
// Subclass Item & Menu shared classes so their items
|
||||
// will be unloaded when add-on is unloaded.
|
||||
exports.Item = makeDisposable(Item);
|
||||
exports.Menu = makeDisposable(Menu);
|
||||
186
toolkit/jetpack/sdk/core/disposable.js
Normal file
186
toolkit/jetpack/sdk/core/disposable.js
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const { Class } = require("./heritage");
|
||||
const { Observer, subscribe, unsubscribe, observe } = require("./observer");
|
||||
const { isWeak } = require("./reference");
|
||||
const SDKWeakSet = require("../lang/weak-set");
|
||||
|
||||
const method = require("../../method/core");
|
||||
|
||||
const unloadSubject = require('@loader/unload');
|
||||
const addonUnloadTopic = "sdk:loader:destroy";
|
||||
|
||||
const uninstall = method("disposable/uninstall");
|
||||
exports.uninstall = uninstall;
|
||||
|
||||
const shutdown = method("disposable/shutdown");
|
||||
exports.shutdown = shutdown;
|
||||
|
||||
const disable = method("disposable/disable");
|
||||
exports.disable = disable;
|
||||
|
||||
const upgrade = method("disposable/upgrade");
|
||||
exports.upgrade = upgrade;
|
||||
|
||||
const downgrade = method("disposable/downgrade");
|
||||
exports.downgrade = downgrade;
|
||||
|
||||
const unload = method("disposable/unload");
|
||||
exports.unload = unload;
|
||||
|
||||
const dispose = method("disposable/dispose");
|
||||
exports.dispose = dispose;
|
||||
dispose.define(Object, object => object.dispose());
|
||||
|
||||
const setup = method("disposable/setup");
|
||||
exports.setup = setup;
|
||||
setup.define(Object, (object, ...args) => object.setup(...args));
|
||||
|
||||
// DisposablesUnloadObserver is the class which subscribe the
|
||||
// Observer Service to be notified when the add-on loader is
|
||||
// unloading to be able to dispose all the existent disposables.
|
||||
const DisposablesUnloadObserver = Class({
|
||||
implements: [Observer],
|
||||
initialize: function(...args) {
|
||||
// Set of the non-weak disposables registered to be disposed.
|
||||
this.disposables = new Set();
|
||||
// Target of the weak disposables registered to be disposed
|
||||
// (and tracked on this target using the SDK weak-set module).
|
||||
this.weakDisposables = {};
|
||||
},
|
||||
subscribe(disposable) {
|
||||
if (isWeak(disposable)) {
|
||||
SDKWeakSet.add(this.weakDisposables, disposable);
|
||||
} else {
|
||||
this.disposables.add(disposable);
|
||||
}
|
||||
},
|
||||
unsubscribe(disposable) {
|
||||
if (isWeak(disposable)) {
|
||||
SDKWeakSet.remove(this.weakDisposables, disposable);
|
||||
} else {
|
||||
this.disposables.delete(disposable);
|
||||
}
|
||||
},
|
||||
tryUnloadDisposable(disposable) {
|
||||
try {
|
||||
if (disposable) {
|
||||
unload(disposable);
|
||||
}
|
||||
} catch(e) {
|
||||
console.error("Error unloading a",
|
||||
isWeak(disposable) ? "weak disposable" : "disposable",
|
||||
disposable, e);
|
||||
}
|
||||
},
|
||||
unloadAll() {
|
||||
// Remove all the subscribed disposables.
|
||||
for (let disposable of this.disposables) {
|
||||
this.tryUnloadDisposable(disposable);
|
||||
}
|
||||
|
||||
this.disposables.clear();
|
||||
|
||||
// Remove all the subscribed weak disposables.
|
||||
for (let disposable of SDKWeakSet.iterator(this.weakDisposables)) {
|
||||
this.tryUnloadDisposable(disposable);
|
||||
}
|
||||
|
||||
SDKWeakSet.clear(this.weakDisposables);
|
||||
}
|
||||
});
|
||||
const disposablesUnloadObserver = new DisposablesUnloadObserver();
|
||||
|
||||
// The DisposablesUnloadObserver instance is the only object which subscribes
|
||||
// the Observer Service directly, it observes add-on unload notifications in
|
||||
// order to trigger `unload` on all its subscribed disposables.
|
||||
observe.define(DisposablesUnloadObserver, (obj, subject, topic, data) => {
|
||||
const isUnloadTopic = topic === addonUnloadTopic;
|
||||
const isUnloadSubject = subject.wrappedJSObject === unloadSubject;
|
||||
if (isUnloadTopic && isUnloadSubject) {
|
||||
unsubscribe(disposablesUnloadObserver, addonUnloadTopic);
|
||||
disposablesUnloadObserver.unloadAll();
|
||||
}
|
||||
});
|
||||
|
||||
subscribe(disposablesUnloadObserver, addonUnloadTopic, false);
|
||||
|
||||
// Set's up disposable instance.
|
||||
const setupDisposable = disposable => {
|
||||
disposablesUnloadObserver.subscribe(disposable);
|
||||
};
|
||||
exports.setupDisposable = setupDisposable;
|
||||
|
||||
// Tears down disposable instance.
|
||||
const disposeDisposable = disposable => {
|
||||
disposablesUnloadObserver.unsubscribe(disposable);
|
||||
};
|
||||
exports.disposeDisposable = disposeDisposable;
|
||||
|
||||
// Base type that takes care of disposing it's instances on add-on unload.
|
||||
// Also makes sure to remove unload listener if it's already being disposed.
|
||||
const Disposable = Class({
|
||||
initialize: function(...args) {
|
||||
// First setup instance before initializing it's disposal. If instance
|
||||
// fails to initialize then there is no instance to be disposed at the
|
||||
// unload.
|
||||
setup(this, ...args);
|
||||
setupDisposable(this);
|
||||
},
|
||||
destroy: function(reason) {
|
||||
// Destroying disposable removes unload handler so that attempt to dispose
|
||||
// won't be made at unload & delegates to dispose.
|
||||
disposeDisposable(this);
|
||||
unload(this, reason);
|
||||
},
|
||||
setup: function() {
|
||||
// Implement your initialize logic here.
|
||||
},
|
||||
dispose: function() {
|
||||
// Implement your cleanup logic here.
|
||||
}
|
||||
});
|
||||
exports.Disposable = Disposable;
|
||||
|
||||
const unloaders = {
|
||||
destroy: dispose,
|
||||
uninstall: uninstall,
|
||||
shutdown: shutdown,
|
||||
disable: disable,
|
||||
upgrade: upgrade,
|
||||
downgrade: downgrade
|
||||
};
|
||||
|
||||
const unloaded = new WeakMap();
|
||||
unload.define(Disposable, (disposable, reason) => {
|
||||
if (!unloaded.get(disposable)) {
|
||||
unloaded.set(disposable, true);
|
||||
// Pick an unload handler associated with an unload
|
||||
// reason (falling back to destroy if not found) and
|
||||
// delegate unloading to it.
|
||||
const unload = unloaders[reason] || unloaders.destroy;
|
||||
unload(disposable);
|
||||
}
|
||||
});
|
||||
|
||||
// If add-on is disabled manually, it's being upgraded, downgraded
|
||||
// or uninstalled `dispose` is invoked to undo any changes that
|
||||
// has being done by it in this session.
|
||||
disable.define(Disposable, dispose);
|
||||
downgrade.define(Disposable, dispose);
|
||||
upgrade.define(Disposable, dispose);
|
||||
uninstall.define(Disposable, dispose);
|
||||
|
||||
// If application is shut down no dispose is invoked as undo-ing
|
||||
// changes made by instance is likely to just waste of resources &
|
||||
// increase shutdown time. Although specefic components may choose
|
||||
// to implement shutdown handler that does something better.
|
||||
shutdown.define(Disposable, disposable => {});
|
||||
184
toolkit/jetpack/sdk/core/heritage.js
Normal file
184
toolkit/jetpack/sdk/core/heritage.js
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
/* 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';
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
var getPrototypeOf = Object.getPrototypeOf;
|
||||
var getNames = x => [...Object.getOwnPropertyNames(x),
|
||||
...Object.getOwnPropertySymbols(x)];
|
||||
var getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
|
||||
var create = Object.create;
|
||||
var freeze = Object.freeze;
|
||||
var unbind = Function.call.bind(Function.bind, Function.call);
|
||||
|
||||
// This shortcut makes sure that we do perform desired operations, even if
|
||||
// associated methods have being overridden on the used object.
|
||||
var owns = unbind(Object.prototype.hasOwnProperty);
|
||||
var apply = unbind(Function.prototype.apply);
|
||||
var slice = Array.slice || unbind(Array.prototype.slice);
|
||||
var reduce = Array.reduce || unbind(Array.prototype.reduce);
|
||||
var map = Array.map || unbind(Array.prototype.map);
|
||||
var concat = Array.concat || unbind(Array.prototype.concat);
|
||||
|
||||
// Utility function to get own properties descriptor map.
|
||||
function getOwnPropertyDescriptors(object) {
|
||||
return reduce(getNames(object), function(descriptor, name) {
|
||||
descriptor[name] = getOwnPropertyDescriptor(object, name);
|
||||
return descriptor;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function isDataProperty(property) {
|
||||
var value = property.value;
|
||||
var type = typeof(property.value);
|
||||
return "value" in property &&
|
||||
(type !== "object" || value === null) &&
|
||||
type !== "function";
|
||||
}
|
||||
|
||||
function getDataProperties(object) {
|
||||
var properties = getOwnPropertyDescriptors(object);
|
||||
return getNames(properties).reduce(function(result, name) {
|
||||
var property = properties[name];
|
||||
if (isDataProperty(property)) {
|
||||
result[name] = {
|
||||
value: property.value,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
enumerable: false
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes `source` object as an argument and returns identical object
|
||||
* with the difference that all own properties will be non-enumerable
|
||||
*/
|
||||
function obscure(source) {
|
||||
var descriptor = reduce(getNames(source), function(descriptor, name) {
|
||||
var property = getOwnPropertyDescriptor(source, name);
|
||||
property.enumerable = false;
|
||||
descriptor[name] = property;
|
||||
return descriptor;
|
||||
}, {});
|
||||
return create(getPrototypeOf(source), descriptor);
|
||||
}
|
||||
exports.obscure = obscure;
|
||||
|
||||
/**
|
||||
* Takes arbitrary number of source objects and returns fresh one, that
|
||||
* inherits from the same prototype as a first argument and implements all
|
||||
* own properties of all argument objects. If two or more argument objects
|
||||
* have own properties with the same name, the property is overridden, with
|
||||
* precedence from right to left, implying, that properties of the object on
|
||||
* the left are overridden by a same named property of the object on the right.
|
||||
*/
|
||||
var mix = function(source) {
|
||||
var descriptor = reduce(slice(arguments), function(descriptor, source) {
|
||||
return reduce(getNames(source), function(descriptor, name) {
|
||||
descriptor[name] = getOwnPropertyDescriptor(source, name);
|
||||
return descriptor;
|
||||
}, descriptor);
|
||||
}, {});
|
||||
|
||||
return create(getPrototypeOf(source), descriptor);
|
||||
};
|
||||
exports.mix = mix;
|
||||
|
||||
/**
|
||||
* Returns a frozen object with that inherits from the given `prototype` and
|
||||
* implements all own properties of the given `properties` object.
|
||||
*/
|
||||
function extend(prototype, properties) {
|
||||
return create(prototype, getOwnPropertyDescriptors(properties));
|
||||
}
|
||||
exports.extend = extend;
|
||||
|
||||
/**
|
||||
* Returns a constructor function with a proper `prototype` setup. Returned
|
||||
* constructor's `prototype` inherits from a given `options.extends` or
|
||||
* `Class.prototype` if omitted and implements all the properties of the
|
||||
* given `option`. If `options.implemens` array is passed, it's elements
|
||||
* will be mixed into prototype as well. Also, `options.extends` can be
|
||||
* a function or a prototype. If function than it's prototype is used as
|
||||
* an ancestor of the prototype, if it's an object that it's used directly.
|
||||
* Also `options.implements` may contain functions or objects, in case of
|
||||
* functions their prototypes are used for mixing.
|
||||
*/
|
||||
var Class = new function() {
|
||||
function prototypeOf(input) {
|
||||
return typeof(input) === 'function' ? input.prototype : input;
|
||||
}
|
||||
var none = freeze([]);
|
||||
|
||||
return function Class(options) {
|
||||
// Create descriptor with normalized `options.extends` and
|
||||
// `options.implements`.
|
||||
var descriptor = {
|
||||
// Normalize extends property of `options.extends` to a prototype object
|
||||
// in case it's constructor. If property is missing that fallback to
|
||||
// `Type.prototype`.
|
||||
extends: owns(options, 'extends') ?
|
||||
prototypeOf(options.extends) : Class.prototype,
|
||||
// Normalize `options.implements` to make sure that it's array of
|
||||
// prototype objects instead of constructor functions.
|
||||
implements: owns(options, 'implements') ?
|
||||
freeze(map(options.implements, prototypeOf)) : none
|
||||
};
|
||||
|
||||
// Create array of property descriptors who's properties will be defined
|
||||
// on the resulting prototype. Note: Using reflection `concat` instead of
|
||||
// method as it may be overridden.
|
||||
var descriptors = concat(descriptor.implements, options, descriptor, {
|
||||
constructor: constructor
|
||||
});
|
||||
|
||||
// Note: we use reflection `apply` in the constructor instead of method
|
||||
// call since later may be overridden.
|
||||
function constructor() {
|
||||
var instance = create(prototype, attributes);
|
||||
if (initialize) apply(initialize, instance, arguments);
|
||||
return instance;
|
||||
}
|
||||
// Create `prototype` that inherits from given ancestor passed as
|
||||
// `options.extends`, falling back to `Type.prototype`, implementing all
|
||||
// properties of given `options.implements` and `options` itself.
|
||||
var prototype = extend(descriptor.extends, mix.apply(mix, descriptors));
|
||||
var initialize = prototype.initialize;
|
||||
|
||||
// Combine ancestor attributes with prototype's attributes so that
|
||||
// ancestors attributes also become initializeable.
|
||||
var attributes = mix(descriptor.extends.constructor.attributes || {},
|
||||
getDataProperties(prototype));
|
||||
|
||||
constructor.attributes = attributes;
|
||||
Object.defineProperty(constructor, 'prototype', {
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: prototype
|
||||
});
|
||||
return constructor;
|
||||
};
|
||||
}
|
||||
Class.prototype = extend(null, obscure({
|
||||
constructor: function constructor() {
|
||||
this.initialize.apply(this, arguments);
|
||||
return this;
|
||||
},
|
||||
initialize: function initialize() {
|
||||
// Do your initialization logic here
|
||||
},
|
||||
// Copy useful properties from `Object.prototype`.
|
||||
toString: Object.prototype.toString,
|
||||
toLocaleString: Object.prototype.toLocaleString,
|
||||
toSource: Object.prototype.toSource,
|
||||
valueOf: Object.prototype.valueOf,
|
||||
isPrototypeOf: Object.prototype.isPrototypeOf
|
||||
}));
|
||||
exports.Class = freeze(Class);
|
||||
43
toolkit/jetpack/sdk/core/namespace.js
Normal file
43
toolkit/jetpack/sdk/core/namespace.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const create = Object.create;
|
||||
const prototypeOf = Object.getPrototypeOf;
|
||||
|
||||
/**
|
||||
* Returns a new namespace, function that may can be used to access an
|
||||
* namespaced object of the argument argument. Namespaced object are associated
|
||||
* with owner objects via weak references. Namespaced objects inherit from the
|
||||
* owners ancestor namespaced object. If owner's ancestor is `null` then
|
||||
* namespaced object inherits from given `prototype`. Namespaces can be used
|
||||
* to define internal APIs that can be shared via enclosing `namespace`
|
||||
* function.
|
||||
* @examples
|
||||
* const internals = ns();
|
||||
* internals(object).secret = secret;
|
||||
*/
|
||||
function ns() {
|
||||
const map = new WeakMap();
|
||||
return function namespace(target) {
|
||||
if (!target) // If `target` is not an object return `target` itself.
|
||||
return target;
|
||||
// If target has no namespaced object yet, create one that inherits from
|
||||
// the target prototype's namespaced object.
|
||||
if (!map.has(target))
|
||||
map.set(target, create(namespace(prototypeOf(target) || null)));
|
||||
|
||||
return map.get(target);
|
||||
};
|
||||
};
|
||||
|
||||
// `Namespace` is a e4x function in the scope, so we export the function also as
|
||||
// `ns` as alias to avoid clashing.
|
||||
exports.ns = ns;
|
||||
exports.Namespace = ns;
|
||||
89
toolkit/jetpack/sdk/core/observer.js
Normal file
89
toolkit/jetpack/sdk/core/observer.js
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
|
||||
const { Cc, Ci, Cr, Cu } = require("chrome");
|
||||
const { Class } = require("./heritage");
|
||||
const { isWeak } = require("./reference");
|
||||
const method = require("../../method/core");
|
||||
|
||||
const observerService = Cc['@mozilla.org/observer-service;1'].
|
||||
getService(Ci.nsIObserverService);
|
||||
|
||||
const { ShimWaiver } = Cu.import("resource://gre/modules/ShimWaiver.jsm");
|
||||
const addObserver = ShimWaiver.getProperty(observerService, "addObserver");
|
||||
const removeObserver = ShimWaiver.getProperty(observerService, "removeObserver");
|
||||
|
||||
// This is a method that will be invoked when notification observer
|
||||
// subscribed to occurs.
|
||||
const observe = method("observer/observe");
|
||||
exports.observe = observe;
|
||||
|
||||
// Method to subscribe to the observer notification.
|
||||
const subscribe = method("observe/subscribe");
|
||||
exports.subscribe = subscribe;
|
||||
|
||||
|
||||
// Method to unsubscribe from the observer notifications.
|
||||
const unsubscribe = method("observer/unsubscribe");
|
||||
exports.unsubscribe = unsubscribe;
|
||||
|
||||
|
||||
// This is wrapper class that takes a `delegate` and produces
|
||||
// instance of `nsIObserver` which will delegate to a given
|
||||
// object when observer notification occurs.
|
||||
const ObserverDelegee = Class({
|
||||
initialize: function(delegate) {
|
||||
this.delegate = delegate;
|
||||
},
|
||||
QueryInterface: function(iid) {
|
||||
if (!iid.equals(Ci.nsIObserver) &&
|
||||
!iid.equals(Ci.nsISupportsWeakReference) &&
|
||||
!iid.equals(Ci.nsISupports))
|
||||
throw Cr.NS_ERROR_NO_INTERFACE;
|
||||
|
||||
return this;
|
||||
},
|
||||
observe: function(subject, topic, data) {
|
||||
observe(this.delegate, subject, topic, data);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Class that can be either mixed in or inherited from in
|
||||
// order to subscribe / unsubscribe for observer notifications.
|
||||
const Observer = Class({});
|
||||
exports.Observer = Observer;
|
||||
|
||||
// Weak maps that associates instance of `ObserverDelegee` with
|
||||
// an actual observer. It ensures that `ObserverDelegee` instance
|
||||
// won't be GC-ed until given `observer` is.
|
||||
const subscribers = new WeakMap();
|
||||
|
||||
// Implementation of `subscribe` for `Observer` type just registers
|
||||
// observer for an observer service. If `isWeak(observer)` is `true`
|
||||
// observer service won't hold strong reference to a given `observer`.
|
||||
subscribe.define(Observer, (observer, topic) => {
|
||||
if (!subscribers.has(observer)) {
|
||||
const delegee = new ObserverDelegee(observer);
|
||||
subscribers.set(observer, delegee);
|
||||
addObserver(delegee, topic, isWeak(observer));
|
||||
}
|
||||
});
|
||||
|
||||
// Unsubscribes `observer` from observer notifications for the
|
||||
// given `topic`.
|
||||
unsubscribe.define(Observer, (observer, topic) => {
|
||||
const delegee = subscribers.get(observer);
|
||||
if (delegee) {
|
||||
subscribers.delete(observer);
|
||||
removeObserver(delegee, topic);
|
||||
}
|
||||
});
|
||||
118
toolkit/jetpack/sdk/core/promise.js
Normal file
118
toolkit/jetpack/sdk/core/promise.js
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
/* 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';
|
||||
|
||||
/*
|
||||
* Uses `Promise.jsm` as a core implementation, with additional sugar
|
||||
* from previous implementation, with inspiration from `Q` and `when`
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm
|
||||
* https://github.com/cujojs/when
|
||||
* https://github.com/kriskowal/q
|
||||
*/
|
||||
const PROMISE_URI = 'resource://gre/modules/Promise.jsm';
|
||||
|
||||
getEnvironment.call(this, function ({ require, exports, module, Cu }) {
|
||||
|
||||
const Promise = Cu.import(PROMISE_URI, {}).Promise;
|
||||
const { Debugging, defer, resolve, all, reject, race } = Promise;
|
||||
|
||||
module.metadata = {
|
||||
'stability': 'unstable'
|
||||
};
|
||||
|
||||
var promised = (function() {
|
||||
// Note: Define shortcuts and utility functions here in order to avoid
|
||||
// slower property accesses and unnecessary closure creations on each
|
||||
// call of this popular function.
|
||||
|
||||
var call = Function.call;
|
||||
var concat = Array.prototype.concat;
|
||||
|
||||
// Utility function that does following:
|
||||
// execute([ f, self, args...]) => f.apply(self, args)
|
||||
function execute (args) {
|
||||
return call.apply(call, args);
|
||||
}
|
||||
|
||||
// Utility function that takes promise of `a` array and maybe promise `b`
|
||||
// as arguments and returns promise for `a.concat(b)`.
|
||||
function promisedConcat(promises, unknown) {
|
||||
return promises.then(function (values) {
|
||||
return resolve(unknown)
|
||||
.then(value => values.concat([value]));
|
||||
});
|
||||
}
|
||||
|
||||
return function promised(f, prototype) {
|
||||
/**
|
||||
Returns a wrapped `f`, which when called returns a promise that resolves to
|
||||
`f(...)` passing all the given arguments to it, which by the way may be
|
||||
promises. Optionally second `prototype` argument may be provided to be used
|
||||
a prototype for a returned promise.
|
||||
|
||||
## Example
|
||||
|
||||
var promise = promised(Array)(1, promise(2), promise(3))
|
||||
promise.then(console.log) // => [ 1, 2, 3 ]
|
||||
**/
|
||||
|
||||
return function promised(...args) {
|
||||
// create array of [ f, this, args... ]
|
||||
return [f, this, ...args].
|
||||
// reduce it via `promisedConcat` to get promised array of fulfillments
|
||||
reduce(promisedConcat, resolve([], prototype)).
|
||||
// finally map that to promise of `f.apply(this, args...)`
|
||||
then(execute);
|
||||
};
|
||||
};
|
||||
})();
|
||||
|
||||
exports.promised = promised;
|
||||
exports.all = all;
|
||||
exports.defer = defer;
|
||||
exports.resolve = resolve;
|
||||
exports.reject = reject;
|
||||
exports.race = race;
|
||||
exports.Promise = Promise;
|
||||
exports.Debugging = Debugging;
|
||||
});
|
||||
|
||||
function getEnvironment (callback) {
|
||||
let Cu, _exports, _module, _require;
|
||||
|
||||
// CommonJS / SDK
|
||||
if (typeof(require) === 'function') {
|
||||
Cu = require('chrome').Cu;
|
||||
_exports = exports;
|
||||
_module = module;
|
||||
_require = require;
|
||||
}
|
||||
// JSM
|
||||
else if (String(this).indexOf('BackstagePass') >= 0) {
|
||||
Cu = this['Components'].utils;
|
||||
_exports = this.Promise = {};
|
||||
_module = { uri: __URI__, id: 'promise/core' };
|
||||
_require = uri => {
|
||||
let imports = {};
|
||||
Cu.import(uri, imports);
|
||||
return imports;
|
||||
};
|
||||
this.EXPORTED_SYMBOLS = ['Promise'];
|
||||
// mozIJSSubScriptLoader.loadSubscript
|
||||
} else if (~String(this).indexOf('Sandbox')) {
|
||||
Cu = this['Components'].utils;
|
||||
_exports = this;
|
||||
_module = { id: 'promise/core' };
|
||||
_require = uri => {};
|
||||
}
|
||||
|
||||
callback({
|
||||
Cu: Cu,
|
||||
exports: _exports,
|
||||
module: _module,
|
||||
require: _require
|
||||
});
|
||||
}
|
||||
|
||||
29
toolkit/jetpack/sdk/core/reference.js
Normal file
29
toolkit/jetpack/sdk/core/reference.js
Normal file
|
|
@ -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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const method = require("../../method/core");
|
||||
const { Class } = require("./heritage");
|
||||
|
||||
// Object that inherit or mix WeakRefence inn will register
|
||||
// weak observes for system notifications.
|
||||
const WeakReference = Class({});
|
||||
exports.WeakReference = WeakReference;
|
||||
|
||||
|
||||
// If `isWeak(object)` is `true` observer installed
|
||||
// for such `object` will be weak, meaning that it will
|
||||
// be GC-ed if nothing else but observer is observing it.
|
||||
// By default everything except `WeakReference` will return
|
||||
// `false`.
|
||||
const isWeak = method("reference/weak?");
|
||||
exports.isWeak = isWeak;
|
||||
|
||||
isWeak.define(Object, _ => false);
|
||||
isWeak.define(WeakReference, _ => true);
|
||||
197
toolkit/jetpack/sdk/deprecated/api-utils.js
Normal file
197
toolkit/jetpack/sdk/deprecated/api-utils.js
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "deprecated"
|
||||
};
|
||||
|
||||
const { merge } = require("../util/object");
|
||||
const { union } = require("../util/array");
|
||||
const { isNil, isRegExp } = require("../lang/type");
|
||||
|
||||
// The possible return values of getTypeOf.
|
||||
const VALID_TYPES = [
|
||||
"array",
|
||||
"boolean",
|
||||
"function",
|
||||
"null",
|
||||
"number",
|
||||
"object",
|
||||
"string",
|
||||
"undefined",
|
||||
"regexp"
|
||||
];
|
||||
|
||||
const { isArray } = Array;
|
||||
|
||||
/**
|
||||
* Returns a validated options dictionary given some requirements. If any of
|
||||
* the requirements are not met, an exception is thrown.
|
||||
*
|
||||
* @param options
|
||||
* An object, the options dictionary to validate. It's not modified.
|
||||
* If it's null or otherwise falsey, an empty object is assumed.
|
||||
* @param requirements
|
||||
* An object whose keys are the expected keys in options. Any key in
|
||||
* options that is not present in requirements is ignored. Each value
|
||||
* in requirements is itself an object describing the requirements of
|
||||
* its key. There are four optional keys in this object:
|
||||
* map: A function that's passed the value of the key in options.
|
||||
* map's return value is taken as the key's value in the final
|
||||
* validated options, is, and ok. If map throws an exception
|
||||
* it's caught and discarded, and the key's value is its value in
|
||||
* options.
|
||||
* is: An array containing any number of the typeof type names. If
|
||||
* the key's value is none of these types, it fails validation.
|
||||
* Arrays, null and regexps are identified by the special type names
|
||||
* "array", "null", "regexp"; "object" will not match either. No type
|
||||
* coercion is done.
|
||||
* ok: A function that's passed the key's value. If it returns
|
||||
* false, the value fails validation.
|
||||
* msg: If the key's value fails validation, an exception is thrown.
|
||||
* This string will be used as its message. If undefined, a
|
||||
* generic message is used, unless is is defined, in which case
|
||||
* the message will state that the value needs to be one of the
|
||||
* given types.
|
||||
* @return An object whose keys are those keys in requirements that are also in
|
||||
* options and whose values are the corresponding return values of map
|
||||
* or the corresponding values in options. Note that any keys not
|
||||
* shared by both requirements and options are not in the returned
|
||||
* object.
|
||||
*/
|
||||
exports.validateOptions = function validateOptions(options, requirements) {
|
||||
options = options || {};
|
||||
let validatedOptions = {};
|
||||
|
||||
for (let key in requirements) {
|
||||
let isOptional = false;
|
||||
let mapThrew = false;
|
||||
let req = requirements[key];
|
||||
let [optsVal, keyInOpts] = (key in options) ?
|
||||
[options[key], true] :
|
||||
[undefined, false];
|
||||
if (req.map) {
|
||||
try {
|
||||
optsVal = req.map(optsVal);
|
||||
}
|
||||
catch (err) {
|
||||
if (err instanceof RequirementError)
|
||||
throw err;
|
||||
|
||||
mapThrew = true;
|
||||
}
|
||||
}
|
||||
if (req.is) {
|
||||
let types = req.is;
|
||||
|
||||
if (!isArray(types) && isArray(types.is))
|
||||
types = types.is;
|
||||
|
||||
if (isArray(types)) {
|
||||
isOptional = ['undefined', 'null'].every(v => ~types.indexOf(v));
|
||||
|
||||
// Sanity check the caller's type names.
|
||||
types.forEach(function (typ) {
|
||||
if (VALID_TYPES.indexOf(typ) < 0) {
|
||||
let msg = 'Internal error: invalid requirement type "' + typ + '".';
|
||||
throw new Error(msg);
|
||||
}
|
||||
});
|
||||
if (types.indexOf(getTypeOf(optsVal)) < 0)
|
||||
throw new RequirementError(key, req);
|
||||
}
|
||||
}
|
||||
|
||||
if (req.ok && ((!isOptional || !isNil(optsVal)) && !req.ok(optsVal)))
|
||||
throw new RequirementError(key, req);
|
||||
|
||||
if (keyInOpts || (req.map && !mapThrew && optsVal !== undefined))
|
||||
validatedOptions[key] = optsVal;
|
||||
}
|
||||
|
||||
return validatedOptions;
|
||||
};
|
||||
|
||||
exports.addIterator = function addIterator(obj, keysValsGenerator) {
|
||||
obj.__iterator__ = function(keysOnly, keysVals) {
|
||||
let keysValsIterator = keysValsGenerator.call(this);
|
||||
|
||||
// "for (.. in ..)" gets only keys, "for each (.. in ..)" gets values,
|
||||
// and "for (.. in Iterator(..))" gets [key, value] pairs.
|
||||
let index = keysOnly ? 0 : 1;
|
||||
while (true)
|
||||
yield keysVals ? keysValsIterator.next() : keysValsIterator.next()[index];
|
||||
};
|
||||
};
|
||||
|
||||
// Similar to typeof, except arrays, null and regexps are identified by "array" and
|
||||
// "null" and "regexp", not "object".
|
||||
var getTypeOf = exports.getTypeOf = function getTypeOf(val) {
|
||||
let typ = typeof(val);
|
||||
if (typ === "object") {
|
||||
if (!val)
|
||||
return "null";
|
||||
if (isArray(val))
|
||||
return "array";
|
||||
if (isRegExp(val))
|
||||
return "regexp";
|
||||
}
|
||||
return typ;
|
||||
}
|
||||
|
||||
function RequirementError(key, requirement) {
|
||||
Error.call(this);
|
||||
|
||||
this.name = "RequirementError";
|
||||
|
||||
let msg = requirement.msg;
|
||||
if (!msg) {
|
||||
msg = 'The option "' + key + '" ';
|
||||
msg += requirement.is ?
|
||||
"must be one of the following types: " + requirement.is.join(", ") :
|
||||
"is invalid.";
|
||||
}
|
||||
|
||||
this.message = msg;
|
||||
}
|
||||
RequirementError.prototype = Object.create(Error.prototype);
|
||||
|
||||
var string = { is: ['string', 'undefined', 'null'] };
|
||||
exports.string = string;
|
||||
|
||||
var number = { is: ['number', 'undefined', 'null'] };
|
||||
exports.number = number;
|
||||
|
||||
var boolean = { is: ['boolean', 'undefined', 'null'] };
|
||||
exports.boolean = boolean;
|
||||
|
||||
var object = { is: ['object', 'undefined', 'null'] };
|
||||
exports.object = object;
|
||||
|
||||
var array = { is: ['array', 'undefined', 'null'] };
|
||||
exports.array = array;
|
||||
|
||||
var isTruthyType = type => !(type === 'undefined' || type === 'null');
|
||||
var findTypes = v => { while (!isArray(v) && v.is) v = v.is; return v };
|
||||
|
||||
function required(req) {
|
||||
let types = (findTypes(req) || VALID_TYPES).filter(isTruthyType);
|
||||
|
||||
return merge({}, req, {is: types});
|
||||
}
|
||||
exports.required = required;
|
||||
|
||||
function optional(req) {
|
||||
req = merge({is: []}, req);
|
||||
req.is = findTypes(req).filter(isTruthyType).concat('undefined', 'null');
|
||||
|
||||
return req;
|
||||
}
|
||||
exports.optional = optional;
|
||||
|
||||
function either(...types) {
|
||||
return union.apply(null, types.map(findTypes));
|
||||
}
|
||||
exports.either = either;
|
||||
54
toolkit/jetpack/sdk/deprecated/events/assembler.js
Normal file
54
toolkit/jetpack/sdk/deprecated/events/assembler.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/* 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 { Class } = require("../../core/heritage");
|
||||
const { removeListener, on } = require("../../dom/events");
|
||||
|
||||
/**
|
||||
* Event targets
|
||||
* can be added / removed by calling `observe / ignore` methods. Composer should
|
||||
* provide array of event types it wishes to handle as property
|
||||
* `supportedEventsTypes` and function for handling all those events as
|
||||
* `handleEvent` property.
|
||||
*/
|
||||
exports.DOMEventAssembler = Class({
|
||||
/**
|
||||
* Function that is supposed to handle all the supported events (that are
|
||||
* present in the `supportedEventsTypes`) from all the observed
|
||||
* `eventTargets`.
|
||||
* @param {Event} event
|
||||
* Event being dispatched.
|
||||
*/
|
||||
handleEvent() {
|
||||
throw new TypeError("Instance of DOMEventAssembler must implement `handleEvent` method");
|
||||
},
|
||||
/**
|
||||
* Array of supported event names.
|
||||
* @type {String[]}
|
||||
*/
|
||||
get supportedEventsTypes() {
|
||||
throw new TypeError("Instance of DOMEventAssembler must implement `handleEvent` field");
|
||||
},
|
||||
/**
|
||||
* Adds `eventTarget` to the list of observed `eventTarget`s. Listeners for
|
||||
* supported events will be registered on the given `eventTarget`.
|
||||
* @param {EventTarget} eventTarget
|
||||
*/
|
||||
observe: function observe(eventTarget) {
|
||||
this.supportedEventsTypes.forEach(function(eventType) {
|
||||
on(eventTarget, eventType, this);
|
||||
}, this);
|
||||
},
|
||||
/**
|
||||
* Removes `eventTarget` from the list of observed `eventTarget`s. Listeners
|
||||
* for all supported events will be unregistered from the given `eventTarget`.
|
||||
* @param {EventTarget} eventTarget
|
||||
*/
|
||||
ignore: function ignore(eventTarget) {
|
||||
this.supportedEventsTypes.forEach(function(eventType) {
|
||||
removeListener(eventTarget, eventType, this);
|
||||
}, this);
|
||||
}
|
||||
});
|
||||
288
toolkit/jetpack/sdk/deprecated/sync-worker.js
Normal file
288
toolkit/jetpack/sdk/deprecated/sync-worker.js
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
/* 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/. */
|
||||
|
||||
/**
|
||||
*
|
||||
* `deprecated/sync-worker` was previously `content/worker`, that was
|
||||
* incompatible with e10s. we are in the process of switching to the new
|
||||
* asynchronous `Worker`, which behaves slightly differently in some edge
|
||||
* cases, so we are keeping this one around for a short period.
|
||||
* try to switch to the new one as soon as possible..
|
||||
*
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { Class } = require('../core/heritage');
|
||||
const { EventTarget } = require('../event/target');
|
||||
const { on, off, emit, setListeners } = require('../event/core');
|
||||
const {
|
||||
attach, detach, destroy
|
||||
} = require('../content/utils');
|
||||
const { method } = require('../lang/functional');
|
||||
const { Ci, Cu, Cc } = require('chrome');
|
||||
const unload = require('../system/unload');
|
||||
const events = require('../system/events');
|
||||
const { getInnerId } = require("../window/utils");
|
||||
const { WorkerSandbox } = require('../content/sandbox');
|
||||
const { isPrivate } = require('../private-browsing/utils');
|
||||
|
||||
// A weak map of workers to hold private attributes that
|
||||
// should not be exposed
|
||||
const workers = new WeakMap();
|
||||
|
||||
var modelFor = (worker) => workers.get(worker);
|
||||
|
||||
const ERR_DESTROYED =
|
||||
"Couldn't find the worker to receive this message. " +
|
||||
"The script may not be initialized yet, or may already have been unloaded.";
|
||||
|
||||
const ERR_FROZEN = "The page is currently hidden and can no longer be used " +
|
||||
"until it is visible again.";
|
||||
|
||||
/**
|
||||
* Message-passing facility for communication between code running
|
||||
* in the content and add-on process.
|
||||
* @see https://developer.mozilla.org/en-US/Add-ons/SDK/Low-Level_APIs/content_worker
|
||||
*/
|
||||
const Worker = Class({
|
||||
implements: [EventTarget],
|
||||
initialize: function WorkerConstructor (options) {
|
||||
// Save model in weak map to not expose properties
|
||||
let model = createModel();
|
||||
workers.set(this, model);
|
||||
|
||||
options = options || {};
|
||||
|
||||
if ('contentScriptFile' in options)
|
||||
this.contentScriptFile = options.contentScriptFile;
|
||||
if ('contentScriptOptions' in options)
|
||||
this.contentScriptOptions = options.contentScriptOptions;
|
||||
if ('contentScript' in options)
|
||||
this.contentScript = options.contentScript;
|
||||
if ('injectInDocument' in options)
|
||||
this.injectInDocument = !!options.injectInDocument;
|
||||
|
||||
setListeners(this, options);
|
||||
|
||||
unload.ensure(this, "destroy");
|
||||
|
||||
// Ensure that worker.port is initialized for contentWorker to be able
|
||||
// to send events during worker initialization.
|
||||
this.port = createPort(this);
|
||||
|
||||
model.documentUnload = documentUnload.bind(this);
|
||||
model.pageShow = pageShow.bind(this);
|
||||
model.pageHide = pageHide.bind(this);
|
||||
|
||||
if ('window' in options)
|
||||
attach(this, options.window);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends a message to the worker's global scope. Method takes single
|
||||
* argument, which represents data to be sent to the worker. The data may
|
||||
* be any primitive type value or `JSON`. Call of this method asynchronously
|
||||
* emits `message` event with data value in the global scope of this
|
||||
* worker.
|
||||
*
|
||||
* `message` event listeners can be set either by calling
|
||||
* `self.on` with a first argument string `"message"` or by
|
||||
* implementing `onMessage` function in the global scope of this worker.
|
||||
* @param {Number|String|JSON} data
|
||||
*/
|
||||
postMessage: function (...data) {
|
||||
let model = modelFor(this);
|
||||
let args = ['message'].concat(data);
|
||||
if (!model.inited) {
|
||||
model.earlyEvents.push(args);
|
||||
return;
|
||||
}
|
||||
processMessage.apply(null, [this].concat(args));
|
||||
},
|
||||
|
||||
get url () {
|
||||
let model = modelFor(this);
|
||||
// model.window will be null after detach
|
||||
return model.window ? model.window.document.location.href : null;
|
||||
},
|
||||
|
||||
get contentURL () {
|
||||
let model = modelFor(this);
|
||||
return model.window ? model.window.document.URL : null;
|
||||
},
|
||||
|
||||
// Implemented to provide some of the previous features of exposing sandbox
|
||||
// so that Worker can be extended
|
||||
getSandbox: function () {
|
||||
return modelFor(this).contentWorker;
|
||||
},
|
||||
|
||||
toString: function () { return '[object Worker]'; },
|
||||
attach: method(attach),
|
||||
detach: method(detach),
|
||||
destroy: method(destroy)
|
||||
});
|
||||
exports.Worker = Worker;
|
||||
|
||||
attach.define(Worker, function (worker, window) {
|
||||
let model = modelFor(worker);
|
||||
model.window = window;
|
||||
// Track document unload to destroy this worker.
|
||||
// We can't watch for unload event on page's window object as it
|
||||
// prevents bfcache from working:
|
||||
// https://developer.mozilla.org/En/Working_with_BFCache
|
||||
model.windowID = getInnerId(model.window);
|
||||
events.on("inner-window-destroyed", model.documentUnload);
|
||||
|
||||
// will set model.contentWorker pointing to the private API:
|
||||
model.contentWorker = WorkerSandbox(worker, model.window);
|
||||
|
||||
// Listen to pagehide event in order to freeze the content script
|
||||
// while the document is frozen in bfcache:
|
||||
model.window.addEventListener("pageshow", model.pageShow, true);
|
||||
model.window.addEventListener("pagehide", model.pageHide, true);
|
||||
|
||||
// Mainly enable worker.port.emit to send event to the content worker
|
||||
model.inited = true;
|
||||
model.frozen = false;
|
||||
|
||||
// Fire off `attach` event
|
||||
emit(worker, 'attach', window);
|
||||
|
||||
// Process all events and messages that were fired before the
|
||||
// worker was initialized.
|
||||
model.earlyEvents.forEach(args => processMessage.apply(null, [worker].concat(args)));
|
||||
});
|
||||
|
||||
/**
|
||||
* Remove all internal references to the attached document
|
||||
* Tells _port to unload itself and removes all the references from itself.
|
||||
*/
|
||||
detach.define(Worker, function (worker, reason) {
|
||||
let model = modelFor(worker);
|
||||
|
||||
// maybe unloaded before content side is created
|
||||
if (model.contentWorker) {
|
||||
model.contentWorker.destroy(reason);
|
||||
}
|
||||
|
||||
model.contentWorker = null;
|
||||
if (model.window) {
|
||||
model.window.removeEventListener("pageshow", model.pageShow, true);
|
||||
model.window.removeEventListener("pagehide", model.pageHide, true);
|
||||
}
|
||||
model.window = null;
|
||||
// This method may be called multiple times,
|
||||
// avoid dispatching `detach` event more than once
|
||||
if (model.windowID) {
|
||||
model.windowID = null;
|
||||
events.off("inner-window-destroyed", model.documentUnload);
|
||||
model.earlyEvents.length = 0;
|
||||
emit(worker, 'detach');
|
||||
}
|
||||
model.inited = false;
|
||||
});
|
||||
|
||||
isPrivate.define(Worker, ({ tab }) => isPrivate(tab));
|
||||
|
||||
/**
|
||||
* Tells content worker to unload itself and
|
||||
* removes all the references from itself.
|
||||
*/
|
||||
destroy.define(Worker, function (worker, reason) {
|
||||
detach(worker, reason);
|
||||
modelFor(worker).inited = true;
|
||||
// Specifying no type or listener removes all listeners
|
||||
// from target
|
||||
off(worker);
|
||||
off(worker.port);
|
||||
});
|
||||
|
||||
/**
|
||||
* Events fired by workers
|
||||
*/
|
||||
function documentUnload ({ subject, data }) {
|
||||
let model = modelFor(this);
|
||||
let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
|
||||
if (innerWinID != model.windowID) return false;
|
||||
detach(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
function pageShow () {
|
||||
let model = modelFor(this);
|
||||
model.contentWorker.emitSync('pageshow');
|
||||
emit(this, 'pageshow');
|
||||
model.frozen = false;
|
||||
}
|
||||
|
||||
function pageHide () {
|
||||
let model = modelFor(this);
|
||||
model.contentWorker.emitSync('pagehide');
|
||||
emit(this, 'pagehide');
|
||||
model.frozen = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired from postMessage and emitEventToContent, or from the earlyMessage
|
||||
* queue when fired before the content is loaded. Sends arguments to
|
||||
* contentWorker if able
|
||||
*/
|
||||
|
||||
function processMessage (worker, ...args) {
|
||||
let model = modelFor(worker) || {};
|
||||
if (!model.contentWorker)
|
||||
throw new Error(ERR_DESTROYED);
|
||||
if (model.frozen)
|
||||
throw new Error(ERR_FROZEN);
|
||||
model.contentWorker.emit.apply(null, args);
|
||||
}
|
||||
|
||||
function createModel () {
|
||||
return {
|
||||
// List of messages fired before worker is initialized
|
||||
earlyEvents: [],
|
||||
// Is worker connected to the content worker sandbox ?
|
||||
inited: false,
|
||||
// Is worker being frozen? i.e related document is frozen in bfcache.
|
||||
// Content script should not be reachable if frozen.
|
||||
frozen: true,
|
||||
/**
|
||||
* Reference to the content side of the worker.
|
||||
* @type {WorkerGlobalScope}
|
||||
*/
|
||||
contentWorker: null,
|
||||
/**
|
||||
* Reference to the window that is accessible from
|
||||
* the content scripts.
|
||||
* @type {Object}
|
||||
*/
|
||||
window: null
|
||||
};
|
||||
}
|
||||
|
||||
function createPort (worker) {
|
||||
let port = EventTarget();
|
||||
port.emit = emitEventToContent.bind(null, worker);
|
||||
return port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a custom event to the content script,
|
||||
* i.e. emit this event on `self.port`
|
||||
*/
|
||||
function emitEventToContent (worker, ...eventArgs) {
|
||||
let model = modelFor(worker);
|
||||
let args = ['event'].concat(eventArgs);
|
||||
if (!model.inited) {
|
||||
model.earlyEvents.push(args);
|
||||
return;
|
||||
}
|
||||
processMessage.apply(null, [worker].concat(args));
|
||||
}
|
||||
199
toolkit/jetpack/sdk/deprecated/unit-test-finder.js
Normal file
199
toolkit/jetpack/sdk/deprecated/unit-test-finder.js
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "deprecated"
|
||||
};
|
||||
|
||||
const file = require("../io/file");
|
||||
const { Loader } = require("../test/loader");
|
||||
|
||||
const { isNative } = require('@loader/options');
|
||||
|
||||
const cuddlefish = isNative ? require("toolkit/loader") : require("../loader/cuddlefish");
|
||||
|
||||
const { defer, resolve } = require("../core/promise");
|
||||
const { getAddon } = require("../addon/installer");
|
||||
const { id } = require("sdk/self");
|
||||
const { newURI } = require('sdk/url/utils');
|
||||
const { getZipReader } = require("../zip/utils");
|
||||
|
||||
const { Cc, Ci, Cu } = require("chrome");
|
||||
const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
|
||||
var ios = Cc['@mozilla.org/network/io-service;1']
|
||||
.getService(Ci.nsIIOService);
|
||||
|
||||
const CFX_TEST_REGEX = /(([^\/]+\/)(?:lib\/)?)?(tests?\/test-[^\.\/]+)\.js$/;
|
||||
const JPM_TEST_REGEX = /^()(tests?\/test-[^\.\/]+)\.js$/;
|
||||
|
||||
const { mapcat, map, filter, fromEnumerator } = require("sdk/util/sequence");
|
||||
|
||||
const toFile = x => x.QueryInterface(Ci.nsIFile);
|
||||
const isTestFile = ({leafName}) => leafName.substr(0, 5) == "test-" && leafName.substr(-3, 3) == ".js";
|
||||
const getFileURI = x => ios.newFileURI(x).spec;
|
||||
|
||||
const getDirectoryEntries = file => map(toFile, fromEnumerator(_ => file.directoryEntries));
|
||||
const getTestFiles = directory => filter(isTestFile, getDirectoryEntries(directory));
|
||||
const getTestURIs = directory => map(getFileURI, getTestFiles(directory));
|
||||
|
||||
const isDirectory = x => x.isDirectory();
|
||||
const getTestEntries = directory => mapcat(entry =>
|
||||
/^tests?$/.test(entry.leafName) ? getTestURIs(entry) : getTestEntries(entry),
|
||||
filter(isDirectory, getDirectoryEntries(directory)));
|
||||
|
||||
const removeDups = (array) => array.reduce((result, value) => {
|
||||
if (value != result[result.length - 1]) {
|
||||
result.push(value);
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
const getSuites = function getSuites({ id, filter }) {
|
||||
const TEST_REGEX = isNative ? JPM_TEST_REGEX : CFX_TEST_REGEX;
|
||||
|
||||
return getAddon(id).then(addon => {
|
||||
let fileURI = addon.getResourceURI("tests/");
|
||||
let isPacked = fileURI.scheme == "jar";
|
||||
let xpiURI = addon.getResourceURI();
|
||||
let file = xpiURI.QueryInterface(Ci.nsIFileURL).file;
|
||||
let suites = [];
|
||||
let addEntry = (entry) => {
|
||||
if (filter(entry) && TEST_REGEX.test(entry)) {
|
||||
let suite = (isNative ? "./" : "") + (RegExp.$2 || "") + RegExp.$3;
|
||||
suites.push(suite);
|
||||
}
|
||||
}
|
||||
|
||||
if (isPacked) {
|
||||
return getZipReader(file).then(zip => {
|
||||
let entries = zip.findEntries(null);
|
||||
while (entries.hasMore()) {
|
||||
let entry = entries.getNext();
|
||||
addEntry(entry);
|
||||
}
|
||||
zip.close();
|
||||
|
||||
// sort and remove dups
|
||||
suites = removeDups(suites.sort());
|
||||
return suites;
|
||||
})
|
||||
}
|
||||
else {
|
||||
let tests = [...getTestEntries(file)];
|
||||
let rootURI = addon.getResourceURI("/");
|
||||
tests.forEach((entry) => {
|
||||
addEntry(entry.replace(rootURI.spec, ""));
|
||||
});
|
||||
}
|
||||
|
||||
// sort and remove dups
|
||||
suites = removeDups(suites.sort());
|
||||
return suites;
|
||||
});
|
||||
}
|
||||
exports.getSuites = getSuites;
|
||||
|
||||
const makeFilters = function makeFilters(options) {
|
||||
options = options || {};
|
||||
|
||||
// A filter string is {fileNameRegex}[:{testNameRegex}] - ie, a colon
|
||||
// optionally separates a regex for the test fileName from a regex for the
|
||||
// testName.
|
||||
if (options.filter) {
|
||||
let colonPos = options.filter.indexOf(':');
|
||||
let filterFileRegex, filterNameRegex;
|
||||
|
||||
if (colonPos === -1) {
|
||||
filterFileRegex = new RegExp(options.filter);
|
||||
filterNameRegex = { test: () => true }
|
||||
}
|
||||
else {
|
||||
filterFileRegex = new RegExp(options.filter.substr(0, colonPos));
|
||||
filterNameRegex = new RegExp(options.filter.substr(colonPos + 1));
|
||||
}
|
||||
|
||||
return {
|
||||
fileFilter: (name) => filterFileRegex.test(name),
|
||||
testFilter: (name) => filterNameRegex.test(name)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
fileFilter: () => true,
|
||||
testFilter: () => true
|
||||
};
|
||||
}
|
||||
exports.makeFilters = makeFilters;
|
||||
|
||||
var loader = Loader(module);
|
||||
const NOT_TESTS = ['setup', 'teardown'];
|
||||
|
||||
var TestFinder = exports.TestFinder = function TestFinder(options) {
|
||||
this.filter = options.filter;
|
||||
this.testInProcess = options.testInProcess === false ? false : true;
|
||||
this.testOutOfProcess = options.testOutOfProcess === true ? true : false;
|
||||
};
|
||||
|
||||
TestFinder.prototype = {
|
||||
findTests: function findTests() {
|
||||
let { fileFilter, testFilter } = makeFilters({ filter: this.filter });
|
||||
|
||||
return getSuites({ id: id, filter: fileFilter }).then(suites => {
|
||||
let testsRemaining = [];
|
||||
|
||||
let getNextTest = () => {
|
||||
if (testsRemaining.length) {
|
||||
return testsRemaining.shift();
|
||||
}
|
||||
|
||||
if (!suites.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let suite = suites.shift();
|
||||
|
||||
// Load each test file as a main module in its own loader instance
|
||||
// `suite` is defined by cuddlefish/manifest.py:ManifestBuilder.build
|
||||
let suiteModule;
|
||||
|
||||
try {
|
||||
suiteModule = cuddlefish.main(loader, suite);
|
||||
}
|
||||
catch (e) {
|
||||
if (/Unsupported Application/i.test(e.message)) {
|
||||
// If `Unsupported Application` error thrown during test,
|
||||
// skip the test suite
|
||||
suiteModule = {
|
||||
'test suite skipped': assert => assert.pass(e.message)
|
||||
};
|
||||
}
|
||||
else {
|
||||
console.exception(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.testInProcess) {
|
||||
for (let name of Object.keys(suiteModule).sort()) {
|
||||
if (NOT_TESTS.indexOf(name) === -1 && testFilter(name)) {
|
||||
testsRemaining.push({
|
||||
setup: suiteModule.setup,
|
||||
teardown: suiteModule.teardown,
|
||||
testFunction: suiteModule[name],
|
||||
name: suite + "." + name
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return getNextTest();
|
||||
};
|
||||
|
||||
return {
|
||||
getNext: () => resolve(getNextTest())
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
584
toolkit/jetpack/sdk/deprecated/unit-test.js
Normal file
584
toolkit/jetpack/sdk/deprecated/unit-test.js
Normal file
|
|
@ -0,0 +1,584 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "deprecated"
|
||||
};
|
||||
|
||||
const timer = require("../timers");
|
||||
const cfxArgs = require("../test/options");
|
||||
const { getTabs, closeTab, getURI, getTabId, getSelectedTab } = require("../tabs/utils");
|
||||
const { windows, isBrowser, getMostRecentBrowserWindow } = require("../window/utils");
|
||||
const { defer, all, Debugging: PromiseDebugging, resolve } = require("../core/promise");
|
||||
const { getInnerId } = require("../window/utils");
|
||||
const { cleanUI } = require("../test/utils");
|
||||
|
||||
const findAndRunTests = function findAndRunTests(options) {
|
||||
var TestFinder = require("./unit-test-finder").TestFinder;
|
||||
var finder = new TestFinder({
|
||||
filter: options.filter,
|
||||
testInProcess: options.testInProcess,
|
||||
testOutOfProcess: options.testOutOfProcess
|
||||
});
|
||||
var runner = new TestRunner({fs: options.fs});
|
||||
finder.findTests().then(tests => {
|
||||
runner.startMany({
|
||||
tests: tests,
|
||||
stopOnError: options.stopOnError,
|
||||
onDone: options.onDone
|
||||
});
|
||||
});
|
||||
};
|
||||
exports.findAndRunTests = findAndRunTests;
|
||||
|
||||
var runnerWindows = new WeakMap();
|
||||
var runnerTabs = new WeakMap();
|
||||
|
||||
const TestRunner = function TestRunner(options) {
|
||||
options = options || {};
|
||||
|
||||
// remember the id's for the open window and tab
|
||||
let window = getMostRecentBrowserWindow();
|
||||
runnerWindows.set(this, getInnerId(window));
|
||||
runnerTabs.set(this, getTabId(getSelectedTab(window)));
|
||||
|
||||
this.fs = options.fs;
|
||||
this.console = options.console || console;
|
||||
this.passed = 0;
|
||||
this.failed = 0;
|
||||
this.testRunSummary = [];
|
||||
this.expectFailNesting = 0;
|
||||
this.done = TestRunner.prototype.done.bind(this);
|
||||
};
|
||||
|
||||
TestRunner.prototype = {
|
||||
toString: function toString() {
|
||||
return "[object TestRunner]";
|
||||
},
|
||||
|
||||
DEFAULT_PAUSE_TIMEOUT: (cfxArgs.parseable ? 300000 : 15000), //Five minutes (5*60*1000ms)
|
||||
PAUSE_DELAY: 500,
|
||||
|
||||
_logTestFailed: function _logTestFailed(why) {
|
||||
if (!(why in this.test.errors))
|
||||
this.test.errors[why] = 0;
|
||||
this.test.errors[why]++;
|
||||
},
|
||||
|
||||
_uncaughtErrorObserver: function({message, date, fileName, stack, lineNumber}) {
|
||||
this.fail("There was an uncaught Promise rejection: " + message + " @ " +
|
||||
fileName + ":" + lineNumber + "\n" + stack);
|
||||
},
|
||||
|
||||
pass: function pass(message) {
|
||||
if(!this.expectFailure) {
|
||||
if ("testMessage" in this.console)
|
||||
this.console.testMessage(true, true, this.test.name, message);
|
||||
else
|
||||
this.console.info("pass:", message);
|
||||
this.passed++;
|
||||
this.test.passed++;
|
||||
this.test.last = message;
|
||||
}
|
||||
else {
|
||||
this.expectFailure = false;
|
||||
this._logTestFailed("failure");
|
||||
if ("testMessage" in this.console) {
|
||||
this.console.testMessage(true, false, this.test.name, message);
|
||||
}
|
||||
else {
|
||||
this.console.error("fail:", 'Failure Expected: ' + message)
|
||||
this.console.trace();
|
||||
}
|
||||
this.failed++;
|
||||
this.test.failed++;
|
||||
}
|
||||
},
|
||||
|
||||
fail: function fail(message) {
|
||||
if(!this.expectFailure) {
|
||||
this._logTestFailed("failure");
|
||||
if ("testMessage" in this.console) {
|
||||
this.console.testMessage(false, false, this.test.name, message);
|
||||
}
|
||||
else {
|
||||
this.console.error("fail:", message)
|
||||
this.console.trace();
|
||||
}
|
||||
this.failed++;
|
||||
this.test.failed++;
|
||||
}
|
||||
else {
|
||||
this.expectFailure = false;
|
||||
if ("testMessage" in this.console)
|
||||
this.console.testMessage(false, true, this.test.name, message);
|
||||
else
|
||||
this.console.info("pass:", message);
|
||||
this.passed++;
|
||||
this.test.passed++;
|
||||
this.test.last = message;
|
||||
}
|
||||
},
|
||||
|
||||
expectFail: function(callback) {
|
||||
this.expectFailure = true;
|
||||
callback();
|
||||
this.expectFailure = false;
|
||||
},
|
||||
|
||||
exception: function exception(e) {
|
||||
this._logTestFailed("exception");
|
||||
if (cfxArgs.parseable)
|
||||
this.console.print("TEST-UNEXPECTED-FAIL | " + this.test.name + " | " + e + "\n");
|
||||
this.console.exception(e);
|
||||
this.failed++;
|
||||
this.test.failed++;
|
||||
},
|
||||
|
||||
assertMatches: function assertMatches(string, regexp, message) {
|
||||
if (regexp.test(string)) {
|
||||
if (!message)
|
||||
message = uneval(string) + " matches " + uneval(regexp);
|
||||
this.pass(message);
|
||||
} else {
|
||||
var no = uneval(string) + " doesn't match " + uneval(regexp);
|
||||
if (!message)
|
||||
message = no;
|
||||
else
|
||||
message = message + " (" + no + ")";
|
||||
this.fail(message);
|
||||
}
|
||||
},
|
||||
|
||||
assertRaises: function assertRaises(func, predicate, message) {
|
||||
try {
|
||||
func();
|
||||
if (message)
|
||||
this.fail(message + " (no exception thrown)");
|
||||
else
|
||||
this.fail("function failed to throw exception");
|
||||
} catch (e) {
|
||||
var errorMessage;
|
||||
if (typeof(e) == "string")
|
||||
errorMessage = e;
|
||||
else
|
||||
errorMessage = e.message;
|
||||
if (typeof(predicate) == "string")
|
||||
this.assertEqual(errorMessage, predicate, message);
|
||||
else
|
||||
this.assertMatches(errorMessage, predicate, message);
|
||||
}
|
||||
},
|
||||
|
||||
assert: function assert(a, message) {
|
||||
if (!a) {
|
||||
if (!message)
|
||||
message = "assertion failed, value is " + a;
|
||||
this.fail(message);
|
||||
} else
|
||||
this.pass(message || "assertion successful");
|
||||
},
|
||||
|
||||
assertNotEqual: function assertNotEqual(a, b, message) {
|
||||
if (a != b) {
|
||||
if (!message)
|
||||
message = "a != b != " + uneval(a);
|
||||
this.pass(message);
|
||||
} else {
|
||||
var equality = uneval(a) + " == " + uneval(b);
|
||||
if (!message)
|
||||
message = equality;
|
||||
else
|
||||
message += " (" + equality + ")";
|
||||
this.fail(message);
|
||||
}
|
||||
},
|
||||
|
||||
assertEqual: function assertEqual(a, b, message) {
|
||||
if (a == b) {
|
||||
if (!message)
|
||||
message = "a == b == " + uneval(a);
|
||||
this.pass(message);
|
||||
} else {
|
||||
var inequality = uneval(a) + " != " + uneval(b);
|
||||
if (!message)
|
||||
message = inequality;
|
||||
else
|
||||
message += " (" + inequality + ")";
|
||||
this.fail(message);
|
||||
}
|
||||
},
|
||||
|
||||
assertNotStrictEqual: function assertNotStrictEqual(a, b, message) {
|
||||
if (a !== b) {
|
||||
if (!message)
|
||||
message = "a !== b !== " + uneval(a);
|
||||
this.pass(message);
|
||||
} else {
|
||||
var equality = uneval(a) + " === " + uneval(b);
|
||||
if (!message)
|
||||
message = equality;
|
||||
else
|
||||
message += " (" + equality + ")";
|
||||
this.fail(message);
|
||||
}
|
||||
},
|
||||
|
||||
assertStrictEqual: function assertStrictEqual(a, b, message) {
|
||||
if (a === b) {
|
||||
if (!message)
|
||||
message = "a === b === " + uneval(a);
|
||||
this.pass(message);
|
||||
} else {
|
||||
var inequality = uneval(a) + " !== " + uneval(b);
|
||||
if (!message)
|
||||
message = inequality;
|
||||
else
|
||||
message += " (" + inequality + ")";
|
||||
this.fail(message);
|
||||
}
|
||||
},
|
||||
|
||||
assertFunction: function assertFunction(a, message) {
|
||||
this.assertStrictEqual('function', typeof a, message);
|
||||
},
|
||||
|
||||
assertUndefined: function(a, message) {
|
||||
this.assertStrictEqual('undefined', typeof a, message);
|
||||
},
|
||||
|
||||
assertNotUndefined: function(a, message) {
|
||||
this.assertNotStrictEqual('undefined', typeof a, message);
|
||||
},
|
||||
|
||||
assertNull: function(a, message) {
|
||||
this.assertStrictEqual(null, a, message);
|
||||
},
|
||||
|
||||
assertNotNull: function(a, message) {
|
||||
this.assertNotStrictEqual(null, a, message);
|
||||
},
|
||||
|
||||
assertObject: function(a, message) {
|
||||
this.assertStrictEqual('[object Object]', Object.prototype.toString.apply(a), message);
|
||||
},
|
||||
|
||||
assertString: function(a, message) {
|
||||
this.assertStrictEqual('[object String]', Object.prototype.toString.apply(a), message);
|
||||
},
|
||||
|
||||
assertArray: function(a, message) {
|
||||
this.assertStrictEqual('[object Array]', Object.prototype.toString.apply(a), message);
|
||||
},
|
||||
|
||||
assertNumber: function(a, message) {
|
||||
this.assertStrictEqual('[object Number]', Object.prototype.toString.apply(a), message);
|
||||
},
|
||||
|
||||
done: function done() {
|
||||
if (this.isDone) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
this.isDone = true;
|
||||
this.pass("This test is done.");
|
||||
|
||||
if (this.test.teardown) {
|
||||
this.test.teardown(this);
|
||||
}
|
||||
|
||||
if (this.waitTimeout !== null) {
|
||||
timer.clearTimeout(this.waitTimeout);
|
||||
this.waitTimeout = null;
|
||||
}
|
||||
|
||||
// Do not leave any callback set when calling to `waitUntil`
|
||||
this.waitUntilCallback = null;
|
||||
if (this.test.passed == 0 && this.test.failed == 0) {
|
||||
this._logTestFailed("empty test");
|
||||
|
||||
if ("testMessage" in this.console) {
|
||||
this.console.testMessage(false, false, this.test.name, "Empty test");
|
||||
}
|
||||
else {
|
||||
this.console.error("fail:", "Empty test")
|
||||
}
|
||||
|
||||
this.failed++;
|
||||
this.test.failed++;
|
||||
}
|
||||
|
||||
let wins = windows(null, { includePrivate: true });
|
||||
let winPromises = wins.map(win => {
|
||||
return new Promise(resolve => {
|
||||
if (["interactive", "complete"].indexOf(win.document.readyState) >= 0) {
|
||||
resolve()
|
||||
}
|
||||
else {
|
||||
win.addEventListener("DOMContentLoaded", function onLoad() {
|
||||
win.removeEventListener("DOMContentLoaded", onLoad, false);
|
||||
resolve();
|
||||
}, false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
PromiseDebugging.flushUncaughtErrors();
|
||||
PromiseDebugging.removeUncaughtErrorObserver(this._uncaughtErrorObserver);
|
||||
|
||||
|
||||
return all(winPromises).then(() => {
|
||||
let browserWins = wins.filter(isBrowser);
|
||||
let tabs = browserWins.reduce((tabs, window) => tabs.concat(getTabs(window)), []);
|
||||
let newTabID = getTabId(getSelectedTab(wins[0]));
|
||||
let oldTabID = runnerTabs.get(this);
|
||||
let hasMoreTabsOpen = browserWins.length && tabs.length != 1;
|
||||
let failure = false;
|
||||
|
||||
if (wins.length != 1 || getInnerId(wins[0]) !== runnerWindows.get(this)) {
|
||||
failure = true;
|
||||
this.fail("Should not be any unexpected windows open");
|
||||
}
|
||||
else if (hasMoreTabsOpen) {
|
||||
failure = true;
|
||||
this.fail("Should not be any unexpected tabs open");
|
||||
}
|
||||
else if (oldTabID != newTabID) {
|
||||
failure = true;
|
||||
runnerTabs.set(this, newTabID);
|
||||
this.fail("Should not be any new tabs left open, old id: " + oldTabID + " new id: " + newTabID);
|
||||
}
|
||||
|
||||
if (failure) {
|
||||
console.log("Windows open:");
|
||||
for (let win of wins) {
|
||||
if (isBrowser(win)) {
|
||||
tabs = getTabs(win);
|
||||
console.log(win.location + " - " + tabs.map(getURI).join(", "));
|
||||
}
|
||||
else {
|
||||
console.log(win.location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return failure;
|
||||
}).
|
||||
then(failure => {
|
||||
if (!failure) {
|
||||
this.pass("There was a clean UI.");
|
||||
return null;
|
||||
}
|
||||
return cleanUI().then(() => {
|
||||
this.pass("There is a clean UI.");
|
||||
});
|
||||
}).
|
||||
then(() => {
|
||||
this.testRunSummary.push({
|
||||
name: this.test.name,
|
||||
passed: this.test.passed,
|
||||
failed: this.test.failed,
|
||||
errors: Object.keys(this.test.errors).join(", ")
|
||||
});
|
||||
|
||||
if (this.onDone !== null) {
|
||||
let onDone = this.onDone;
|
||||
this.onDone = null;
|
||||
timer.setTimeout(_ => onDone(this));
|
||||
}
|
||||
}).
|
||||
catch(console.exception);
|
||||
},
|
||||
|
||||
// Set of assertion functions to wait for an assertion to become true
|
||||
// These functions take the same arguments as the TestRunner.assert* methods.
|
||||
waitUntil: function waitUntil() {
|
||||
return this._waitUntil(this.assert, arguments);
|
||||
},
|
||||
|
||||
waitUntilNotEqual: function waitUntilNotEqual() {
|
||||
return this._waitUntil(this.assertNotEqual, arguments);
|
||||
},
|
||||
|
||||
waitUntilEqual: function waitUntilEqual() {
|
||||
return this._waitUntil(this.assertEqual, arguments);
|
||||
},
|
||||
|
||||
waitUntilMatches: function waitUntilMatches() {
|
||||
return this._waitUntil(this.assertMatches, arguments);
|
||||
},
|
||||
|
||||
/**
|
||||
* Internal function that waits for an assertion to become true.
|
||||
* @param {Function} assertionMethod
|
||||
* Reference to a TestRunner assertion method like test.assert,
|
||||
* test.assertEqual, ...
|
||||
* @param {Array} args
|
||||
* List of arguments to give to the previous assertion method.
|
||||
* All functions in this list are going to be called to retrieve current
|
||||
* assertion values.
|
||||
*/
|
||||
_waitUntil: function waitUntil(assertionMethod, args) {
|
||||
let { promise, resolve } = defer();
|
||||
let count = 0;
|
||||
let maxCount = this.DEFAULT_PAUSE_TIMEOUT / this.PAUSE_DELAY;
|
||||
|
||||
// We need to ensure that test is asynchronous
|
||||
if (!this.waitTimeout)
|
||||
this.waitUntilDone(this.DEFAULT_PAUSE_TIMEOUT);
|
||||
|
||||
let finished = false;
|
||||
let test = this;
|
||||
|
||||
// capture a traceback before we go async.
|
||||
let traceback = require("../console/traceback");
|
||||
let stack = traceback.get();
|
||||
stack.splice(-2, 2);
|
||||
let currentWaitStack = traceback.format(stack);
|
||||
let timeout = null;
|
||||
|
||||
function loop(stopIt) {
|
||||
timeout = null;
|
||||
|
||||
// Build a mockup object to fake TestRunner API and intercept calls to
|
||||
// pass and fail methods, in order to retrieve nice error messages
|
||||
// and assertion result
|
||||
let mock = {
|
||||
pass: function (msg) {
|
||||
test.pass(msg);
|
||||
test.waitUntilCallback = null;
|
||||
if (!stopIt)
|
||||
resolve();
|
||||
},
|
||||
fail: function (msg) {
|
||||
// If we are called on test timeout, we stop the loop
|
||||
// and print which test keeps failing:
|
||||
if (stopIt) {
|
||||
test.console.error("test assertion never became true:\n",
|
||||
msg + "\n",
|
||||
currentWaitStack);
|
||||
if (timeout)
|
||||
timer.clearTimeout(timeout);
|
||||
return;
|
||||
}
|
||||
timeout = timer.setTimeout(loop, test.PAUSE_DELAY);
|
||||
}
|
||||
};
|
||||
|
||||
// Automatically call args closures in order to build arguments for
|
||||
// assertion function
|
||||
let appliedArgs = [];
|
||||
for (let i = 0, l = args.length; i < l; i++) {
|
||||
let a = args[i];
|
||||
if (typeof a == "function") {
|
||||
try {
|
||||
a = a();
|
||||
}
|
||||
catch(e) {
|
||||
test.fail("Exception when calling asynchronous assertion: " + e +
|
||||
"\n" + e.stack);
|
||||
return resolve();
|
||||
}
|
||||
}
|
||||
appliedArgs.push(a);
|
||||
}
|
||||
|
||||
// Finally call assertion function with current assertion values
|
||||
assertionMethod.apply(mock, appliedArgs);
|
||||
}
|
||||
loop();
|
||||
this.waitUntilCallback = loop;
|
||||
|
||||
return promise;
|
||||
},
|
||||
|
||||
waitUntilDone: function waitUntilDone(ms) {
|
||||
if (ms === undefined)
|
||||
ms = this.DEFAULT_PAUSE_TIMEOUT;
|
||||
|
||||
var self = this;
|
||||
|
||||
function tiredOfWaiting() {
|
||||
self._logTestFailed("timed out");
|
||||
if ("testMessage" in self.console) {
|
||||
self.console.testMessage(false, false, self.test.name,
|
||||
`Test timed out (after: ${self.test.last})`);
|
||||
}
|
||||
else {
|
||||
self.console.error("fail:", `Timed out (after: ${self.test.last})`)
|
||||
}
|
||||
if (self.waitUntilCallback) {
|
||||
self.waitUntilCallback(true);
|
||||
self.waitUntilCallback = null;
|
||||
}
|
||||
self.failed++;
|
||||
self.test.failed++;
|
||||
self.done();
|
||||
}
|
||||
|
||||
// We may already have registered a timeout callback
|
||||
if (this.waitTimeout)
|
||||
timer.clearTimeout(this.waitTimeout);
|
||||
|
||||
this.waitTimeout = timer.setTimeout(tiredOfWaiting, ms);
|
||||
},
|
||||
|
||||
startMany: function startMany(options) {
|
||||
function runNextTest(self) {
|
||||
let { tests, onDone } = options;
|
||||
|
||||
return tests.getNext().then((test) => {
|
||||
if (options.stopOnError && self.test && self.test.failed) {
|
||||
self.console.error("aborted: test failed and --stop-on-error was specified");
|
||||
onDone(self);
|
||||
}
|
||||
else if (test) {
|
||||
self.start({test: test, onDone: runNextTest});
|
||||
}
|
||||
else {
|
||||
onDone(self);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return runNextTest(this).catch(console.exception);
|
||||
},
|
||||
|
||||
start: function start(options) {
|
||||
this.test = options.test;
|
||||
this.test.passed = 0;
|
||||
this.test.failed = 0;
|
||||
this.test.errors = {};
|
||||
this.test.last = 'START';
|
||||
PromiseDebugging.clearUncaughtErrorObservers();
|
||||
this._uncaughtErrorObserver = this._uncaughtErrorObserver.bind(this);
|
||||
PromiseDebugging.addUncaughtErrorObserver(this._uncaughtErrorObserver);
|
||||
|
||||
this.isDone = false;
|
||||
this.onDone = function(self) {
|
||||
if (cfxArgs.parseable)
|
||||
self.console.print("TEST-END | " + self.test.name + "\n");
|
||||
options.onDone(self);
|
||||
}
|
||||
this.waitTimeout = null;
|
||||
|
||||
try {
|
||||
if (cfxArgs.parseable)
|
||||
this.console.print("TEST-START | " + this.test.name + "\n");
|
||||
else
|
||||
this.console.info("executing '" + this.test.name + "'");
|
||||
|
||||
if(this.test.setup) {
|
||||
this.test.setup(this);
|
||||
}
|
||||
this.test.testFunction(this);
|
||||
} catch (e) {
|
||||
this.exception(e);
|
||||
}
|
||||
if (this.waitTimeout === null)
|
||||
this.done();
|
||||
}
|
||||
};
|
||||
exports.TestRunner = TestRunner;
|
||||
193
toolkit/jetpack/sdk/deprecated/window-utils.js
Normal file
193
toolkit/jetpack/sdk/deprecated/window-utils.js
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
/* 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';
|
||||
|
||||
module.metadata = {
|
||||
'stability': 'deprecated'
|
||||
};
|
||||
|
||||
const { Cc, Ci } = require('chrome');
|
||||
const events = require('../system/events');
|
||||
const { getInnerId, getOuterId, windows, isDocumentLoaded, isBrowser,
|
||||
getMostRecentBrowserWindow, getToplevelWindow, getMostRecentWindow } = require('../window/utils');
|
||||
const { deprecateFunction } = require('../util/deprecate');
|
||||
const { ignoreWindow } = require('sdk/private-browsing/utils');
|
||||
const { isPrivateBrowsingSupported } = require('../self');
|
||||
|
||||
const windowWatcher = Cc['@mozilla.org/embedcomp/window-watcher;1'].
|
||||
getService(Ci.nsIWindowWatcher);
|
||||
const appShellService = Cc['@mozilla.org/appshell/appShellService;1'].
|
||||
getService(Ci.nsIAppShellService);
|
||||
|
||||
// Bug 834961: ignore private windows when they are not supported
|
||||
function getWindows() {
|
||||
return windows(null, { includePrivate: isPrivateBrowsingSupported });
|
||||
}
|
||||
|
||||
/**
|
||||
* An iterator for XUL windows currently in the application.
|
||||
*
|
||||
* @return A generator that yields XUL windows exposing the
|
||||
* nsIDOMWindow interface.
|
||||
*/
|
||||
function windowIterator() {
|
||||
// Bug 752631: We only pass already loaded window in order to avoid
|
||||
// breaking XUL windows DOM. DOM is broken when some JS code try
|
||||
// to access DOM during "uninitialized" state of the related document.
|
||||
let list = getWindows().filter(isDocumentLoaded);
|
||||
for (let i = 0, l = list.length; i < l; i++) {
|
||||
yield list[i];
|
||||
}
|
||||
};
|
||||
exports.windowIterator = windowIterator;
|
||||
|
||||
/**
|
||||
* An iterator for browser windows currently open in the application.
|
||||
* @returns {Function}
|
||||
* A generator that yields browser windows exposing the `nsIDOMWindow`
|
||||
* interface.
|
||||
*/
|
||||
function browserWindowIterator() {
|
||||
for (let window of windowIterator()) {
|
||||
if (isBrowser(window))
|
||||
yield window;
|
||||
}
|
||||
}
|
||||
exports.browserWindowIterator = browserWindowIterator;
|
||||
|
||||
function WindowTracker(delegate) {
|
||||
if (!(this instanceof WindowTracker)) {
|
||||
return new WindowTracker(delegate);
|
||||
}
|
||||
|
||||
this._delegate = delegate;
|
||||
|
||||
for (let window of getWindows())
|
||||
this._regWindow(window);
|
||||
windowWatcher.registerNotification(this);
|
||||
this._onToplevelWindowReady = this._onToplevelWindowReady.bind(this);
|
||||
events.on('toplevel-window-ready', this._onToplevelWindowReady);
|
||||
|
||||
require('../system/unload').ensure(this);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
WindowTracker.prototype = {
|
||||
_regLoadingWindow: function _regLoadingWindow(window) {
|
||||
// Bug 834961: ignore private windows when they are not supported
|
||||
if (ignoreWindow(window))
|
||||
return;
|
||||
|
||||
window.addEventListener('load', this, true);
|
||||
},
|
||||
|
||||
_unregLoadingWindow: function _unregLoadingWindow(window) {
|
||||
// This may have no effect if we ignored the window in _regLoadingWindow().
|
||||
window.removeEventListener('load', this, true);
|
||||
},
|
||||
|
||||
_regWindow: function _regWindow(window) {
|
||||
// Bug 834961: ignore private windows when they are not supported
|
||||
if (ignoreWindow(window))
|
||||
return;
|
||||
|
||||
if (window.document.readyState == 'complete') {
|
||||
this._unregLoadingWindow(window);
|
||||
this._delegate.onTrack(window);
|
||||
} else
|
||||
this._regLoadingWindow(window);
|
||||
},
|
||||
|
||||
_unregWindow: function _unregWindow(window) {
|
||||
if (window.document.readyState == 'complete') {
|
||||
if (this._delegate.onUntrack)
|
||||
this._delegate.onUntrack(window);
|
||||
} else {
|
||||
this._unregLoadingWindow(window);
|
||||
}
|
||||
},
|
||||
|
||||
unload: function unload() {
|
||||
windowWatcher.unregisterNotification(this);
|
||||
events.off('toplevel-window-ready', this._onToplevelWindowReady);
|
||||
for (let window of getWindows())
|
||||
this._unregWindow(window);
|
||||
},
|
||||
|
||||
handleEvent: function handleEvent(event) {
|
||||
try {
|
||||
if (event.type == 'load' && event.target) {
|
||||
var window = event.target.defaultView;
|
||||
if (window)
|
||||
this._regWindow(getToplevelWindow(window));
|
||||
}
|
||||
}
|
||||
catch(e) {
|
||||
console.exception(e);
|
||||
}
|
||||
},
|
||||
|
||||
_onToplevelWindowReady: function _onToplevelWindowReady({subject}) {
|
||||
let window = getToplevelWindow(subject);
|
||||
// ignore private windows if they are not supported
|
||||
if (ignoreWindow(window))
|
||||
return;
|
||||
this._regWindow(window);
|
||||
},
|
||||
|
||||
observe: function observe(subject, topic, data) {
|
||||
try {
|
||||
var window = subject.QueryInterface(Ci.nsIDOMWindow);
|
||||
// ignore private windows if they are not supported
|
||||
if (ignoreWindow(window))
|
||||
return;
|
||||
if (topic == 'domwindowclosed')
|
||||
this._unregWindow(window);
|
||||
}
|
||||
catch(e) {
|
||||
console.exception(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
exports.WindowTracker = WindowTracker;
|
||||
|
||||
Object.defineProperties(exports, {
|
||||
activeWindow: {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
return getMostRecentWindow(null);
|
||||
},
|
||||
set: function(window) {
|
||||
try {
|
||||
window.focus();
|
||||
} catch (e) {}
|
||||
}
|
||||
},
|
||||
activeBrowserWindow: {
|
||||
enumerable: true,
|
||||
get: getMostRecentBrowserWindow
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Returns the ID of the window's current inner window.
|
||||
*/
|
||||
exports.getInnerId = deprecateFunction(getInnerId,
|
||||
'require("window-utils").getInnerId is deprecated, ' +
|
||||
'please use require("sdk/window/utils").getInnerId instead'
|
||||
);
|
||||
|
||||
exports.getOuterId = deprecateFunction(getOuterId,
|
||||
'require("window-utils").getOuterId is deprecated, ' +
|
||||
'please use require("sdk/window/utils").getOuterId instead'
|
||||
);
|
||||
|
||||
exports.isBrowser = deprecateFunction(isBrowser,
|
||||
'require("window-utils").isBrowser is deprecated, ' +
|
||||
'please use require("sdk/window/utils").isBrowser instead'
|
||||
);
|
||||
|
||||
exports.hiddenWindow = appShellService.hiddenDOMWindow;
|
||||
18
toolkit/jetpack/sdk/dom/events-shimmed.js
Normal file
18
toolkit/jetpack/sdk/dom/events-shimmed.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/* 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';
|
||||
|
||||
module.metadata = {
|
||||
'stability': 'unstable'
|
||||
};
|
||||
|
||||
const events = require('./events.js');
|
||||
|
||||
exports.emit = (element, type, obj) => events.emit(element, type, obj, true);
|
||||
exports.on = (element, type, listener, capture) => events.on(element, type, listener, capture, true);
|
||||
exports.once = (element, type, listener, capture) => events.once(element, type, listener, capture, true);
|
||||
exports.removeListener = (element, type, listener, capture) => events.removeListener(element, type, listener, capture, true);
|
||||
exports.removed = events.removed;
|
||||
exports.when = (element, eventName, capture) => events.when(element, eventName, capture ? capture : false, true);
|
||||
192
toolkit/jetpack/sdk/dom/events.js
Normal file
192
toolkit/jetpack/sdk/dom/events.js
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
const { ShimWaiver } = Cu.import("resource://gre/modules/ShimWaiver.jsm");
|
||||
|
||||
// Utility function that returns copy of the given `text` with last character
|
||||
// removed if it is `"s"`.
|
||||
function singularify(text) {
|
||||
return text[text.length - 1] === "s" ? text.substr(0, text.length - 1) : text;
|
||||
}
|
||||
|
||||
// Utility function that takes event type, argument is passed to
|
||||
// `document.createEvent` and returns name of the initializer method of the
|
||||
// given event. Please note that there are some event types whose initializer
|
||||
// methods can't be guessed by this function. For more details see following
|
||||
// link: https://developer.mozilla.org/En/DOM/Document.createEvent
|
||||
function getInitializerName(category) {
|
||||
return "init" + singularify(category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an event `listener` on a given `element`, that will be called
|
||||
* when events of specified `type` is dispatched on the `element`.
|
||||
* @param {Element} element
|
||||
* Dom element to register listener on.
|
||||
* @param {String} type
|
||||
* A string representing the
|
||||
* [event type](https://developer.mozilla.org/en/DOM/event.type) to
|
||||
* listen for.
|
||||
* @param {Function} listener
|
||||
* Function that is called whenever an event of the specified `type`
|
||||
* occurs.
|
||||
* @param {Boolean} capture
|
||||
* If true, indicates that the user wishes to initiate capture. After
|
||||
* initiating capture, all events of the specified type will be dispatched
|
||||
* to the registered listener before being dispatched to any `EventTarget`s
|
||||
* beneath it in the DOM tree. Events which are bubbling upward through
|
||||
* the tree will not trigger a listener designated to use capture.
|
||||
* See [DOM Level 3 Events](http://www.w3.org/TR/DOM-Level-3-Events/#event-flow)
|
||||
* for a detailed explanation.
|
||||
*/
|
||||
function on(element, type, listener, capture, shimmed = false) {
|
||||
// `capture` defaults to `false`.
|
||||
capture = capture || false;
|
||||
if (shimmed) {
|
||||
element.addEventListener(type, listener, capture);
|
||||
} else {
|
||||
ShimWaiver.getProperty(element, "addEventListener")(type, listener, capture);
|
||||
}
|
||||
}
|
||||
exports.on = on;
|
||||
|
||||
/**
|
||||
* Registers an event `listener` on a given `element`, that will be called
|
||||
* only once, next time event of specified `type` is dispatched on the
|
||||
* `element`.
|
||||
* @param {Element} element
|
||||
* Dom element to register listener on.
|
||||
* @param {String} type
|
||||
* A string representing the
|
||||
* [event type](https://developer.mozilla.org/en/DOM/event.type) to
|
||||
* listen for.
|
||||
* @param {Function} listener
|
||||
* Function that is called whenever an event of the specified `type`
|
||||
* occurs.
|
||||
* @param {Boolean} capture
|
||||
* If true, indicates that the user wishes to initiate capture. After
|
||||
* initiating capture, all events of the specified type will be dispatched
|
||||
* to the registered listener before being dispatched to any `EventTarget`s
|
||||
* beneath it in the DOM tree. Events which are bubbling upward through
|
||||
* the tree will not trigger a listener designated to use capture.
|
||||
* See [DOM Level 3 Events](http://www.w3.org/TR/DOM-Level-3-Events/#event-flow)
|
||||
* for a detailed explanation.
|
||||
*/
|
||||
function once(element, type, listener, capture, shimmed = false) {
|
||||
on(element, type, function selfRemovableListener(event) {
|
||||
removeListener(element, type, selfRemovableListener, capture, shimmed);
|
||||
listener.apply(this, arguments);
|
||||
}, capture, shimmed);
|
||||
}
|
||||
exports.once = once;
|
||||
|
||||
/**
|
||||
* Unregisters an event `listener` on a given `element` for the events of the
|
||||
* specified `type`.
|
||||
*
|
||||
* @param {Element} element
|
||||
* Dom element to unregister listener from.
|
||||
* @param {String} type
|
||||
* A string representing the
|
||||
* [event type](https://developer.mozilla.org/en/DOM/event.type) to
|
||||
* listen for.
|
||||
* @param {Function} listener
|
||||
* Function that is called whenever an event of the specified `type`
|
||||
* occurs.
|
||||
* @param {Boolean} capture
|
||||
* If true, indicates that the user wishes to initiate capture. After
|
||||
* initiating capture, all events of the specified type will be dispatched
|
||||
* to the registered listener before being dispatched to any `EventTarget`s
|
||||
* beneath it in the DOM tree. Events which are bubbling upward through
|
||||
* the tree will not trigger a listener designated to use capture.
|
||||
* See [DOM Level 3 Events](http://www.w3.org/TR/DOM-Level-3-Events/#event-flow)
|
||||
* for a detailed explanation.
|
||||
*/
|
||||
function removeListener(element, type, listener, capture, shimmed = false) {
|
||||
if (shimmed) {
|
||||
element.removeEventListener(type, listener, capture);
|
||||
} else {
|
||||
ShimWaiver.getProperty(element, "removeEventListener")(type, listener, capture);
|
||||
}
|
||||
}
|
||||
exports.removeListener = removeListener;
|
||||
|
||||
/**
|
||||
* Emits event of the specified `type` and `category` on the given `element`.
|
||||
* Specified `settings` are used to initialize event before dispatching it.
|
||||
* @param {Element} element
|
||||
* Dom element to dispatch event on.
|
||||
* @param {String} type
|
||||
* A string representing the
|
||||
* [event type](https://developer.mozilla.org/en/DOM/event.type).
|
||||
* @param {Object} options
|
||||
* Options object containing following properties:
|
||||
* - `category`: String passed to the `document.createEvent`. Option is
|
||||
* optional and defaults to "UIEvents".
|
||||
* - `initializer`: If passed it will be used as name of the method used
|
||||
* to initialize event. If omitted name will be generated from the
|
||||
* `category` field by prefixing it with `"init"` and removing last
|
||||
* character if it matches `"s"`.
|
||||
* - `settings`: Array of settings that are forwarded to the event
|
||||
* initializer after firs `type` argument.
|
||||
* @see https://developer.mozilla.org/En/DOM/Document.createEvent
|
||||
*/
|
||||
function emit(element, type, { category, initializer, settings }, shimmed = false) {
|
||||
category = category || "UIEvents";
|
||||
initializer = initializer || getInitializerName(category);
|
||||
let document = element.ownerDocument;
|
||||
let event = document.createEvent(category);
|
||||
event[initializer].apply(event, [type].concat(settings));
|
||||
if (shimmed) {
|
||||
element.dispatchEvent(event);
|
||||
} else {
|
||||
ShimWaiver.getProperty(element, "dispatchEvent")(event);
|
||||
}
|
||||
};
|
||||
exports.emit = emit;
|
||||
|
||||
// Takes DOM `element` and returns promise which is resolved
|
||||
// when given element is removed from it's parent node.
|
||||
const removed = element => {
|
||||
return new Promise(resolve => {
|
||||
const { MutationObserver } = element.ownerDocument.defaultView;
|
||||
const observer = new MutationObserver(mutations => {
|
||||
for (let mutation of mutations) {
|
||||
for (let node of mutation.removedNodes || []) {
|
||||
if (node === element) {
|
||||
observer.disconnect();
|
||||
resolve(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe(element.parentNode, {childList: true});
|
||||
});
|
||||
};
|
||||
exports.removed = removed;
|
||||
|
||||
const when = (element, eventName, capture=false, shimmed=false) => new Promise(resolve => {
|
||||
const listener = event => {
|
||||
if (shimmed) {
|
||||
element.removeEventListener(eventName, listener, capture);
|
||||
} else {
|
||||
ShimWaiver.getProperty(element, "removeEventListener")(eventName, listener, capture);
|
||||
}
|
||||
resolve(event);
|
||||
};
|
||||
|
||||
if (shimmed) {
|
||||
element.addEventListener(eventName, listener, capture);
|
||||
} else {
|
||||
ShimWaiver.getProperty(element, "addEventListener")(eventName, listener, capture);
|
||||
}
|
||||
});
|
||||
exports.when = when;
|
||||
63
toolkit/jetpack/sdk/dom/events/keys.js
Normal file
63
toolkit/jetpack/sdk/dom/events/keys.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { emit } = require("../events");
|
||||
const { getCodeForKey, toJSON } = require("../../keyboard/utils");
|
||||
const { has } = require("../../util/array");
|
||||
const { isString } = require("../../lang/type");
|
||||
|
||||
const INITIALIZER = "initKeyEvent";
|
||||
const CATEGORY = "KeyboardEvent";
|
||||
|
||||
function Options(options) {
|
||||
if (!isString(options))
|
||||
return options;
|
||||
|
||||
var { key, modifiers } = toJSON(options);
|
||||
return {
|
||||
key: key,
|
||||
control: has(modifiers, "control"),
|
||||
alt: has(modifiers, "alt"),
|
||||
shift: has(modifiers, "shift"),
|
||||
meta: has(modifiers, "meta")
|
||||
};
|
||||
}
|
||||
|
||||
var keyEvent = exports.keyEvent = function keyEvent(element, type, options) {
|
||||
|
||||
emit(element, type, {
|
||||
initializer: INITIALIZER,
|
||||
category: CATEGORY,
|
||||
settings: [
|
||||
!("bubbles" in options) || options.bubbles !== false,
|
||||
!("cancelable" in options) || options.cancelable !== false,
|
||||
"window" in options && options.window ? options.window : null,
|
||||
"control" in options && !!options.control,
|
||||
"alt" in options && !!options.alt,
|
||||
"shift" in options && !!options.shift,
|
||||
"meta" in options && !!options.meta,
|
||||
getCodeForKey(options.key) || 0,
|
||||
options.key.length === 1 ? options.key.charCodeAt(0) : 0
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
exports.keyDown = function keyDown(element, options) {
|
||||
keyEvent(element, "keydown", Options(options));
|
||||
};
|
||||
|
||||
exports.keyUp = function keyUp(element, options) {
|
||||
keyEvent(element, "keyup", Options(options));
|
||||
};
|
||||
|
||||
exports.keyPress = function keyPress(element, options) {
|
||||
keyEvent(element, "keypress", Options(options));
|
||||
};
|
||||
|
||||
65
toolkit/jetpack/sdk/event/chrome.js
Normal file
65
toolkit/jetpack/sdk/event/chrome.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { Cc, Ci, Cr, Cu } = require("chrome");
|
||||
const { emit, on, off } = require("./core");
|
||||
var observerService = Cc["@mozilla.org/observer-service;1"]
|
||||
.getService(Ci.nsIObserverService);
|
||||
|
||||
const { ShimWaiver } = Cu.import("resource://gre/modules/ShimWaiver.jsm");
|
||||
const addObserver = ShimWaiver.getProperty(observerService, "addObserver");
|
||||
const removeObserver = ShimWaiver.getProperty(observerService, "removeObserver");
|
||||
|
||||
const { when: unload } = require("../system/unload");
|
||||
|
||||
// Simple class that can be used to instantiate event channel that
|
||||
// implements `nsIObserver` interface. It's will is used by `observe`
|
||||
// function as observer + event target. It basically proxies observer
|
||||
// notifications as to it's registered listeners.
|
||||
function ObserverChannel() {}
|
||||
Object.freeze(Object.defineProperties(ObserverChannel.prototype, {
|
||||
QueryInterface: {
|
||||
value: function(iid) {
|
||||
if (!iid.equals(Ci.nsIObserver) &&
|
||||
!iid.equals(Ci.nsISupportsWeakReference) &&
|
||||
!iid.equals(Ci.nsISupports))
|
||||
throw Cr.NS_ERROR_NO_INTERFACE;
|
||||
return this;
|
||||
}
|
||||
},
|
||||
observe: {
|
||||
value: function(subject, topic, data) {
|
||||
emit(this, "data", {
|
||||
type: topic,
|
||||
target: subject,
|
||||
data: data
|
||||
});
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
function observe(topic) {
|
||||
let observerChannel = new ObserverChannel();
|
||||
|
||||
// Note: `nsIObserverService` will not hold a weak reference to a
|
||||
// observerChannel (since third argument is `true`). There for if it
|
||||
// will be GC-ed with all it's event listeners once no other references
|
||||
// will be held.
|
||||
addObserver(observerChannel, topic, true);
|
||||
|
||||
// We need to remove any observer added once the add-on is unloaded;
|
||||
// otherwise we'll get a "dead object" exception.
|
||||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1001833
|
||||
unload(() => removeObserver(observerChannel, topic));
|
||||
|
||||
return observerChannel;
|
||||
}
|
||||
|
||||
exports.observe = observe;
|
||||
193
toolkit/jetpack/sdk/event/core.js
Normal file
193
toolkit/jetpack/sdk/event/core.js
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const UNCAUGHT_ERROR = 'An error event was emitted for which there was no listener.';
|
||||
const BAD_LISTENER = 'The event listener must be a function.';
|
||||
|
||||
const { ns } = require('../core/namespace');
|
||||
|
||||
const event = ns();
|
||||
|
||||
const EVENT_TYPE_PATTERN = /^on([A-Z]\w+$)/;
|
||||
exports.EVENT_TYPE_PATTERN = EVENT_TYPE_PATTERN;
|
||||
|
||||
// Utility function to access given event `target` object's event listeners for
|
||||
// the specific event `type`. If listeners for this type does not exists they
|
||||
// will be created.
|
||||
const observers = function observers(target, type) {
|
||||
if (!target) throw TypeError("Event target must be an object");
|
||||
let listeners = event(target);
|
||||
return type in listeners ? listeners[type] : listeners[type] = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers an event `listener` that is called every time events of
|
||||
* specified `type` is emitted on the given event `target`.
|
||||
* @param {Object} target
|
||||
* Event target object.
|
||||
* @param {String} type
|
||||
* The type of event.
|
||||
* @param {Function} listener
|
||||
* The listener function that processes the event.
|
||||
*/
|
||||
function on(target, type, listener) {
|
||||
if (typeof(listener) !== 'function')
|
||||
throw new Error(BAD_LISTENER);
|
||||
|
||||
let listeners = observers(target, type);
|
||||
if (!~listeners.indexOf(listener))
|
||||
listeners.push(listener);
|
||||
}
|
||||
exports.on = on;
|
||||
|
||||
|
||||
var onceWeakMap = new WeakMap();
|
||||
|
||||
|
||||
/**
|
||||
* Registers an event `listener` that is called only the next time an event
|
||||
* of the specified `type` is emitted on the given event `target`.
|
||||
* @param {Object} target
|
||||
* Event target object.
|
||||
* @param {String} type
|
||||
* The type of the event.
|
||||
* @param {Function} listener
|
||||
* The listener function that processes the event.
|
||||
*/
|
||||
function once(target, type, listener) {
|
||||
let replacement = function observer(...args) {
|
||||
off(target, type, observer);
|
||||
onceWeakMap.delete(listener);
|
||||
listener.apply(target, args);
|
||||
};
|
||||
onceWeakMap.set(listener, replacement);
|
||||
on(target, type, replacement);
|
||||
}
|
||||
exports.once = once;
|
||||
|
||||
/**
|
||||
* Execute each of the listeners in order with the supplied arguments.
|
||||
* All the exceptions that are thrown by listeners during the emit
|
||||
* are caught and can be handled by listeners of 'error' event. Thrown
|
||||
* exceptions are passed as an argument to an 'error' event listener.
|
||||
* If no 'error' listener is registered exception will be logged into an
|
||||
* error console.
|
||||
* @param {Object} target
|
||||
* Event target object.
|
||||
* @param {String} type
|
||||
* The type of event.
|
||||
* @params {Object|Number|String|Boolean} args
|
||||
* Arguments that will be passed to listeners.
|
||||
*/
|
||||
function emit (target, type, ...args) {
|
||||
emitOnObject(target, type, target, ...args);
|
||||
}
|
||||
exports.emit = emit;
|
||||
|
||||
/**
|
||||
* A variant of emit that allows setting the this property for event listeners
|
||||
*/
|
||||
function emitOnObject(target, type, thisArg, ...args) {
|
||||
let all = observers(target, '*').length;
|
||||
let state = observers(target, type);
|
||||
let listeners = state.slice();
|
||||
let count = listeners.length;
|
||||
let index = 0;
|
||||
|
||||
// If error event and there are no handlers (explicit or catch-all)
|
||||
// then print error message to the console.
|
||||
if (count === 0 && type === 'error' && all === 0)
|
||||
console.exception(args[0]);
|
||||
while (index < count) {
|
||||
try {
|
||||
let listener = listeners[index];
|
||||
// Dispatch only if listener is still registered.
|
||||
if (~state.indexOf(listener))
|
||||
listener.apply(thisArg, args);
|
||||
}
|
||||
catch (error) {
|
||||
// If exception is not thrown by a error listener and error listener is
|
||||
// registered emit `error` event. Otherwise dump exception to the console.
|
||||
if (type !== 'error') emit(target, 'error', error);
|
||||
else console.exception(error);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
// Also emit on `"*"` so that one could listen for all events.
|
||||
if (type !== '*') emit(target, '*', type, ...args);
|
||||
}
|
||||
exports.emitOnObject = emitOnObject;
|
||||
|
||||
/**
|
||||
* Removes an event `listener` for the given event `type` on the given event
|
||||
* `target`. If no `listener` is passed removes all listeners of the given
|
||||
* `type`. If `type` is not passed removes all the listeners of the given
|
||||
* event `target`.
|
||||
* @param {Object} target
|
||||
* The event target object.
|
||||
* @param {String} type
|
||||
* The type of event.
|
||||
* @param {Function} listener
|
||||
* The listener function that processes the event.
|
||||
*/
|
||||
function off(target, type, listener) {
|
||||
let length = arguments.length;
|
||||
if (length === 3) {
|
||||
if (onceWeakMap.has(listener)) {
|
||||
listener = onceWeakMap.get(listener);
|
||||
onceWeakMap.delete(listener);
|
||||
}
|
||||
|
||||
let listeners = observers(target, type);
|
||||
let index = listeners.indexOf(listener);
|
||||
if (~index)
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
else if (length === 2) {
|
||||
observers(target, type).splice(0);
|
||||
}
|
||||
else if (length === 1) {
|
||||
let listeners = event(target);
|
||||
Object.keys(listeners).forEach(type => delete listeners[type]);
|
||||
}
|
||||
}
|
||||
exports.off = off;
|
||||
|
||||
/**
|
||||
* Returns a number of event listeners registered for the given event `type`
|
||||
* on the given event `target`.
|
||||
*/
|
||||
function count(target, type) {
|
||||
return observers(target, type).length;
|
||||
}
|
||||
exports.count = count;
|
||||
|
||||
/**
|
||||
* Registers listeners on the given event `target` from the given `listeners`
|
||||
* dictionary. Iterates over the listeners and if property name matches name
|
||||
* pattern `onEventType` and property is a function, then registers it as
|
||||
* an `eventType` listener on `target`.
|
||||
*
|
||||
* @param {Object} target
|
||||
* The type of event.
|
||||
* @param {Object} listeners
|
||||
* Dictionary of listeners.
|
||||
*/
|
||||
function setListeners(target, listeners) {
|
||||
Object.keys(listeners || {}).forEach(key => {
|
||||
let match = EVENT_TYPE_PATTERN.exec(key);
|
||||
let type = match && match[1].toLowerCase();
|
||||
if (!type) return;
|
||||
|
||||
let listener = listeners[key];
|
||||
if (typeof(listener) === 'function')
|
||||
on(target, type, listener);
|
||||
});
|
||||
}
|
||||
exports.setListeners = setListeners;
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue