mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-10 02:08:38 +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
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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue