michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: /** michael@0: * Here's the server side of the remote inspector. michael@0: * michael@0: * The WalkerActor is the client's view of the debuggee's DOM. It's gives michael@0: * the client a tree of NodeActor objects. michael@0: * michael@0: * The walker presents the DOM tree mostly unmodified from the source DOM michael@0: * tree, but with a few key differences: michael@0: * michael@0: * - Empty text nodes are ignored. This is pretty typical of developer michael@0: * tools, but maybe we should reconsider that on the server side. michael@0: * - iframes with documents loaded have the loaded document as the child, michael@0: * the walker provides one big tree for the whole document tree. michael@0: * michael@0: * There are a few ways to get references to NodeActors: michael@0: * michael@0: * - When you first get a WalkerActor reference, it comes with a free michael@0: * reference to the root document's node. michael@0: * - Given a node, you can ask for children, siblings, and parents. michael@0: * - You can issue querySelector and querySelectorAll requests to find michael@0: * other elements. michael@0: * - Requests that return arbitrary nodes from the tree (like querySelector michael@0: * and querySelectorAll) will also return any nodes the client hasn't michael@0: * seen in order to have a complete set of parents. michael@0: * michael@0: * Once you have a NodeFront, you should be able to answer a few questions michael@0: * without further round trips, like the node's name, namespace/tagName, michael@0: * attributes, etc. Other questions (like a text node's full nodeValue) michael@0: * might require another round trip. michael@0: * michael@0: * The protocol guarantees that the client will always know the parent of michael@0: * any node that is returned by the server. This means that some requests michael@0: * (like querySelector) will include the extra nodes needed to satisfy this michael@0: * requirement. The client keeps track of this parent relationship, so the michael@0: * node fronts form a tree that is a subset of the actual DOM tree. michael@0: * michael@0: * michael@0: * We maintain this guarantee to support the ability to release subtrees on michael@0: * the client - when a node is disconnected from the DOM tree we want to be michael@0: * able to free the client objects for all the children nodes. michael@0: * michael@0: * So to be able to answer "all the children of a given node that we have michael@0: * seen on the client side", we guarantee that every time we've seen a node, michael@0: * we connect it up through its parents. michael@0: */ michael@0: michael@0: const {Cc, Ci, Cu, Cr} = require("chrome"); michael@0: const Services = require("Services"); michael@0: const protocol = require("devtools/server/protocol"); michael@0: const {Arg, Option, method, RetVal, types} = protocol; michael@0: const {LongStringActor, ShortLongString} = require("devtools/server/actors/string"); michael@0: const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); michael@0: const object = require("sdk/util/object"); michael@0: const events = require("sdk/event/core"); michael@0: const {Unknown} = require("sdk/platform/xpcom"); michael@0: const {Class} = require("sdk/core/heritage"); michael@0: const {PageStyleActor} = require("devtools/server/actors/styles"); michael@0: const {HighlighterActor} = require("devtools/server/actors/highlighter"); michael@0: michael@0: const PSEUDO_CLASSES = [":hover", ":active", ":focus"]; michael@0: const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__"; michael@0: const XHTML_NS = "http://www.w3.org/1999/xhtml"; michael@0: const IMAGE_FETCHING_TIMEOUT = 500; michael@0: // The possible completions to a ':' with added score to give certain values michael@0: // some preference. michael@0: const PSEUDO_SELECTORS = [ michael@0: [":active", 1], michael@0: [":hover", 1], michael@0: [":focus", 1], michael@0: [":visited", 0], michael@0: [":link", 0], michael@0: [":first-letter", 0], michael@0: [":first-child", 2], michael@0: [":before", 2], michael@0: [":after", 2], michael@0: [":lang(", 0], michael@0: [":not(", 3], michael@0: [":first-of-type", 0], michael@0: [":last-of-type", 0], michael@0: [":only-of-type", 0], michael@0: [":only-child", 2], michael@0: [":nth-child(", 3], michael@0: [":nth-last-child(", 0], michael@0: [":nth-of-type(", 0], michael@0: [":nth-last-of-type(", 0], michael@0: [":last-child", 2], michael@0: [":root", 0], michael@0: [":empty", 0], michael@0: [":target", 0], michael@0: [":enabled", 0], michael@0: [":disabled", 0], michael@0: [":checked", 1], michael@0: ["::selection", 0] michael@0: ]; michael@0: michael@0: michael@0: let HELPER_SHEET = ".__fx-devtools-hide-shortcut__ { visibility: hidden !important } "; michael@0: HELPER_SHEET += ":-moz-devtools-highlighted { outline: 2px dashed #F06!important; outline-offset: -2px!important } "; michael@0: michael@0: Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm"); michael@0: michael@0: loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm"); michael@0: michael@0: loader.lazyGetter(this, "DOMParser", function() { michael@0: return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser); michael@0: }); michael@0: michael@0: exports.register = function(handle) { michael@0: handle.addGlobalActor(InspectorActor, "inspectorActor"); michael@0: handle.addTabActor(InspectorActor, "inspectorActor"); michael@0: }; michael@0: michael@0: exports.unregister = function(handle) { michael@0: handle.removeGlobalActor(InspectorActor); michael@0: handle.removeTabActor(InspectorActor); michael@0: }; michael@0: michael@0: // XXX: A poor man's makeInfallible until we move it out of transport.js michael@0: // Which should be very soon. michael@0: function makeInfallible(handler) { michael@0: return function(...args) { michael@0: try { michael@0: return handler.apply(this, args); michael@0: } catch(ex) { michael@0: console.error(ex); michael@0: } michael@0: return undefined; michael@0: } michael@0: } michael@0: michael@0: // A resolve that hits the main loop first. michael@0: function delayedResolve(value) { michael@0: let deferred = promise.defer(); michael@0: Services.tm.mainThread.dispatch(makeInfallible(function delayedResolveHandler() { michael@0: deferred.resolve(value); michael@0: }), 0); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: types.addDictType("imageData", { michael@0: // The image data michael@0: data: "nullable:longstring", michael@0: // The original image dimensions michael@0: size: "json" michael@0: }); michael@0: michael@0: /** michael@0: * We only send nodeValue up to a certain size by default. This stuff michael@0: * controls that size. michael@0: */ michael@0: exports.DEFAULT_VALUE_SUMMARY_LENGTH = 50; michael@0: var gValueSummaryLength = exports.DEFAULT_VALUE_SUMMARY_LENGTH; michael@0: michael@0: exports.getValueSummaryLength = function() { michael@0: return gValueSummaryLength; michael@0: }; michael@0: michael@0: exports.setValueSummaryLength = function(val) { michael@0: gValueSummaryLength = val; michael@0: }; michael@0: michael@0: /** michael@0: * Server side of the node actor. michael@0: */ michael@0: var NodeActor = exports.NodeActor = protocol.ActorClass({ michael@0: typeName: "domnode", michael@0: michael@0: initialize: function(walker, node) { michael@0: protocol.Actor.prototype.initialize.call(this, null); michael@0: this.walker = walker; michael@0: this.rawNode = node; michael@0: }, michael@0: michael@0: toString: function() { michael@0: return "[NodeActor " + this.actorID + " for " + this.rawNode.toString() + "]"; michael@0: }, michael@0: michael@0: /** michael@0: * Instead of storing a connection object, the NodeActor gets its connection michael@0: * from its associated walker. michael@0: */ michael@0: get conn() this.walker.conn, michael@0: michael@0: isDocumentElement: function() { michael@0: return this.rawNode.ownerDocument && michael@0: this.rawNode.ownerDocument.documentElement === this.rawNode; michael@0: }, michael@0: michael@0: // Returns the JSON representation of this object over the wire. michael@0: form: function(detail) { michael@0: if (detail === "actorid") { michael@0: return this.actorID; michael@0: } michael@0: michael@0: let parentNode = this.walker.parentNode(this); michael@0: michael@0: // Estimate the number of children. michael@0: let numChildren = this.rawNode.childNodes.length; michael@0: if (numChildren === 0 && michael@0: (this.rawNode.contentDocument || this.rawNode.getSVGDocument)) { michael@0: // This might be an iframe with virtual children. michael@0: numChildren = 1; michael@0: } michael@0: michael@0: let form = { michael@0: actor: this.actorID, michael@0: baseURI: this.rawNode.baseURI, michael@0: parent: parentNode ? parentNode.actorID : undefined, michael@0: nodeType: this.rawNode.nodeType, michael@0: namespaceURI: this.rawNode.namespaceURI, michael@0: nodeName: this.rawNode.nodeName, michael@0: numChildren: numChildren, michael@0: michael@0: // doctype attributes michael@0: name: this.rawNode.name, michael@0: publicId: this.rawNode.publicId, michael@0: systemId: this.rawNode.systemId, michael@0: michael@0: attrs: this.writeAttrs(), michael@0: michael@0: pseudoClassLocks: this.writePseudoClassLocks(), michael@0: }; michael@0: michael@0: if (this.isDocumentElement()) { michael@0: form.isDocumentElement = true; michael@0: } michael@0: michael@0: if (this.rawNode.nodeValue) { michael@0: // We only include a short version of the value if it's longer than michael@0: // gValueSummaryLength michael@0: if (this.rawNode.nodeValue.length > gValueSummaryLength) { michael@0: form.shortValue = this.rawNode.nodeValue.substring(0, gValueSummaryLength); michael@0: form.incompleteValue = true; michael@0: } else { michael@0: form.shortValue = this.rawNode.nodeValue; michael@0: } michael@0: } michael@0: michael@0: return form; michael@0: }, michael@0: michael@0: writeAttrs: function() { michael@0: if (!this.rawNode.attributes) { michael@0: return undefined; michael@0: } michael@0: return [{namespace: attr.namespace, name: attr.name, value: attr.value } michael@0: for (attr of this.rawNode.attributes)]; michael@0: }, michael@0: michael@0: writePseudoClassLocks: function() { michael@0: if (this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) { michael@0: return undefined; michael@0: } michael@0: let ret = undefined; michael@0: for (let pseudo of PSEUDO_CLASSES) { michael@0: if (DOMUtils.hasPseudoClassLock(this.rawNode, pseudo)) { michael@0: ret = ret || []; michael@0: ret.push(pseudo); michael@0: } michael@0: } michael@0: return ret; michael@0: }, michael@0: michael@0: /** michael@0: * Returns a LongStringActor with the node's value. michael@0: */ michael@0: getNodeValue: method(function() { michael@0: return new LongStringActor(this.conn, this.rawNode.nodeValue || ""); michael@0: }, { michael@0: request: {}, michael@0: response: { michael@0: value: RetVal("longstring") michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Set the node's value to a given string. michael@0: */ michael@0: setNodeValue: method(function(value) { michael@0: this.rawNode.nodeValue = value; michael@0: }, { michael@0: request: { value: Arg(0) }, michael@0: response: {} michael@0: }), michael@0: michael@0: /** michael@0: * Get the node's image data if any (for canvas and img nodes). michael@0: * Returns an imageData object with the actual data being a LongStringActor michael@0: * and a size json object. michael@0: * The image data is transmitted as a base64 encoded png data-uri. michael@0: * The method rejects if the node isn't an image or if the image is missing michael@0: * michael@0: * Accepts a maxDim request parameter to resize images that are larger. This michael@0: * is important as the resizing occurs server-side so that image-data being michael@0: * transfered in the longstring back to the client will be that much smaller michael@0: */ michael@0: getImageData: method(function(maxDim) { michael@0: // imageToImageData may fail if the node isn't an image michael@0: try { michael@0: let imageData = imageToImageData(this.rawNode, maxDim); michael@0: return promise.resolve({ michael@0: data: LongStringActor(this.conn, imageData.data), michael@0: size: imageData.size michael@0: }); michael@0: } catch(e) { michael@0: return promise.reject(new Error("Image not available")); michael@0: } michael@0: }, { michael@0: request: {maxDim: Arg(0, "nullable:number")}, michael@0: response: RetVal("imageData") michael@0: }), michael@0: michael@0: /** michael@0: * Modify a node's attributes. Passed an array of modifications michael@0: * similar in format to "attributes" mutations. michael@0: * { michael@0: * attributeName: michael@0: * attributeNamespace: michael@0: * newValue: - If null or undefined, the attribute michael@0: * will be removed. michael@0: * } michael@0: * michael@0: * Returns when the modifications have been made. Mutations will michael@0: * be queued for any changes made. michael@0: */ michael@0: modifyAttributes: method(function(modifications) { michael@0: let rawNode = this.rawNode; michael@0: for (let change of modifications) { michael@0: if (change.newValue == null) { michael@0: if (change.attributeNamespace) { michael@0: rawNode.removeAttributeNS(change.attributeNamespace, change.attributeName); michael@0: } else { michael@0: rawNode.removeAttribute(change.attributeName); michael@0: } michael@0: } else { michael@0: if (change.attributeNamespace) { michael@0: rawNode.setAttributeNS(change.attributeNamespace, change.attributeName, change.newValue); michael@0: } else { michael@0: rawNode.setAttribute(change.attributeName, change.newValue); michael@0: } michael@0: } michael@0: } michael@0: }, { michael@0: request: { michael@0: modifications: Arg(0, "array:json") michael@0: }, michael@0: response: {} michael@0: }) michael@0: }); michael@0: michael@0: /** michael@0: * Client side of the node actor. michael@0: * michael@0: * Node fronts are strored in a tree that mirrors the DOM tree on the michael@0: * server, but with a few key differences: michael@0: * - Not all children will be necessary loaded for each node. michael@0: * - The order of children isn't guaranteed to be the same as the DOM. michael@0: * Children are stored in a doubly-linked list, to make addition/removal michael@0: * and traversal quick. michael@0: * michael@0: * Due to the order/incompleteness of the child list, it is safe to use michael@0: * the parent node from clients, but the `children` request should be used michael@0: * to traverse children. michael@0: */ michael@0: let NodeFront = protocol.FrontClass(NodeActor, { michael@0: initialize: function(conn, form, detail, ctx) { michael@0: this._parent = null; // The parent node michael@0: this._child = null; // The first child of this node. michael@0: this._next = null; // The next sibling of this node. michael@0: this._prev = null; // The previous sibling of this node. michael@0: protocol.Front.prototype.initialize.call(this, conn, form, detail, ctx); michael@0: }, michael@0: michael@0: /** michael@0: * Destroy a node front. The node must have been removed from the michael@0: * ownership tree before this is called, unless the whole walker front michael@0: * is being destroyed. michael@0: */ michael@0: destroy: function() { michael@0: // If an observer was added on this node, shut it down. michael@0: if (this.observer) { michael@0: this.observer.disconnect(); michael@0: this.observer = null; michael@0: } michael@0: michael@0: protocol.Front.prototype.destroy.call(this); michael@0: }, michael@0: michael@0: // Update the object given a form representation off the wire. michael@0: form: function(form, detail, ctx) { michael@0: if (detail === "actorid") { michael@0: this.actorID = form; michael@0: return; michael@0: } michael@0: // Shallow copy of the form. We could just store a reference, but michael@0: // eventually we'll want to update some of the data. michael@0: this._form = object.merge(form); michael@0: this._form.attrs = this._form.attrs ? this._form.attrs.slice() : []; michael@0: michael@0: if (form.parent) { michael@0: // Get the owner actor for this actor (the walker), and find the michael@0: // parent node of this actor from it, creating a standin node if michael@0: // necessary. michael@0: let parentNodeFront = ctx.marshallPool().ensureParentFront(form.parent); michael@0: this.reparent(parentNodeFront); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Returns the parent NodeFront for this NodeFront. michael@0: */ michael@0: parentNode: function() { michael@0: return this._parent; michael@0: }, michael@0: michael@0: /** michael@0: * Process a mutation entry as returned from the walker's `getMutations` michael@0: * request. Only tries to handle changes of the node's contents michael@0: * themselves (character data and attribute changes), the walker itself michael@0: * will keep the ownership tree up to date. michael@0: */ michael@0: updateMutation: function(change) { michael@0: if (change.type === "attributes") { michael@0: // We'll need to lazily reparse the attributes after this change. michael@0: this._attrMap = undefined; michael@0: michael@0: // Update any already-existing attributes. michael@0: let found = false; michael@0: for (let i = 0; i < this.attributes.length; i++) { michael@0: let attr = this.attributes[i]; michael@0: if (attr.name == change.attributeName && michael@0: attr.namespace == change.attributeNamespace) { michael@0: if (change.newValue !== null) { michael@0: attr.value = change.newValue; michael@0: } else { michael@0: this.attributes.splice(i, 1); michael@0: } michael@0: found = true; michael@0: break; michael@0: } michael@0: } michael@0: // This is a new attribute. michael@0: if (!found) { michael@0: this.attributes.push({ michael@0: name: change.attributeName, michael@0: namespace: change.attributeNamespace, michael@0: value: change.newValue michael@0: }); michael@0: } michael@0: } else if (change.type === "characterData") { michael@0: this._form.shortValue = change.newValue; michael@0: this._form.incompleteValue = change.incompleteValue; michael@0: } else if (change.type === "pseudoClassLock") { michael@0: this._form.pseudoClassLocks = change.pseudoClassLocks; michael@0: } michael@0: }, michael@0: michael@0: // Some accessors to make NodeFront feel more like an nsIDOMNode michael@0: michael@0: get id() this.getAttribute("id"), michael@0: michael@0: get nodeType() this._form.nodeType, michael@0: get namespaceURI() this._form.namespaceURI, michael@0: get nodeName() this._form.nodeName, michael@0: michael@0: get baseURI() this._form.baseURI, michael@0: michael@0: get className() { michael@0: return this.getAttribute("class") || ''; michael@0: }, michael@0: michael@0: get hasChildren() this._form.numChildren > 0, michael@0: get numChildren() this._form.numChildren, michael@0: michael@0: get tagName() this.nodeType === Ci.nsIDOMNode.ELEMENT_NODE ? this.nodeName : null, michael@0: get shortValue() this._form.shortValue, michael@0: get incompleteValue() !!this._form.incompleteValue, michael@0: michael@0: get isDocumentElement() !!this._form.isDocumentElement, michael@0: michael@0: // doctype properties michael@0: get name() this._form.name, michael@0: get publicId() this._form.publicId, michael@0: get systemId() this._form.systemId, michael@0: michael@0: getAttribute: function(name) { michael@0: let attr = this._getAttribute(name); michael@0: return attr ? attr.value : null; michael@0: }, michael@0: hasAttribute: function(name) { michael@0: this._cacheAttributes(); michael@0: return (name in this._attrMap); michael@0: }, michael@0: michael@0: get hidden() { michael@0: let cls = this.getAttribute("class"); michael@0: return cls && cls.indexOf(HIDDEN_CLASS) > -1; michael@0: }, michael@0: michael@0: get attributes() this._form.attrs, michael@0: michael@0: get pseudoClassLocks() this._form.pseudoClassLocks || [], michael@0: hasPseudoClassLock: function(pseudo) { michael@0: return this.pseudoClassLocks.some(locked => locked === pseudo); michael@0: }, michael@0: michael@0: getNodeValue: protocol.custom(function() { michael@0: if (!this.incompleteValue) { michael@0: return delayedResolve(new ShortLongString(this.shortValue)); michael@0: } else { michael@0: return this._getNodeValue(); michael@0: } michael@0: }, { michael@0: impl: "_getNodeValue" michael@0: }), michael@0: michael@0: /** michael@0: * Return a new AttributeModificationList for this node. michael@0: */ michael@0: startModifyingAttributes: function() { michael@0: return AttributeModificationList(this); michael@0: }, michael@0: michael@0: _cacheAttributes: function() { michael@0: if (typeof(this._attrMap) != "undefined") { michael@0: return; michael@0: } michael@0: this._attrMap = {}; michael@0: for (let attr of this.attributes) { michael@0: this._attrMap[attr.name] = attr; michael@0: } michael@0: }, michael@0: michael@0: _getAttribute: function(name) { michael@0: this._cacheAttributes(); michael@0: return this._attrMap[name] || undefined; michael@0: }, michael@0: michael@0: /** michael@0: * Set this node's parent. Note that the children saved in michael@0: * this tree are unordered and incomplete, so shouldn't be used michael@0: * instead of a `children` request. michael@0: */ michael@0: reparent: function(parent) { michael@0: if (this._parent === parent) { michael@0: return; michael@0: } michael@0: michael@0: if (this._parent && this._parent._child === this) { michael@0: this._parent._child = this._next; michael@0: } michael@0: if (this._prev) { michael@0: this._prev._next = this._next; michael@0: } michael@0: if (this._next) { michael@0: this._next._prev = this._prev; michael@0: } michael@0: this._next = null; michael@0: this._prev = null; michael@0: this._parent = parent; michael@0: if (!parent) { michael@0: // Subtree is disconnected, we're done michael@0: return; michael@0: } michael@0: this._next = parent._child; michael@0: if (this._next) { michael@0: this._next._prev = this; michael@0: } michael@0: parent._child = this; michael@0: }, michael@0: michael@0: /** michael@0: * Return all the known children of this node. michael@0: */ michael@0: treeChildren: function() { michael@0: let ret = []; michael@0: for (let child = this._child; child != null; child = child._next) { michael@0: ret.push(child); michael@0: } michael@0: return ret; michael@0: }, michael@0: michael@0: /** michael@0: * Do we use a local target? michael@0: * Useful to know if a rawNode is available or not. michael@0: * michael@0: * This will, one day, be removed. External code should michael@0: * not need to know if the target is remote or not. michael@0: */ michael@0: isLocal_toBeDeprecated: function() { michael@0: return !!this.conn._transport._serverConnection; michael@0: }, michael@0: michael@0: /** michael@0: * Get an nsIDOMNode for the given node front. This only works locally, michael@0: * and is only intended as a stopgap during the transition to the remote michael@0: * protocol. If you depend on this you're likely to break soon. michael@0: */ michael@0: rawNode: function(rawNode) { michael@0: if (!this.conn._transport._serverConnection) { michael@0: console.warn("Tried to use rawNode on a remote connection."); michael@0: return null; michael@0: } michael@0: let actor = this.conn._transport._serverConnection.getActor(this.actorID); michael@0: if (!actor) { michael@0: // Can happen if we try to get the raw node for an already-expired michael@0: // actor. michael@0: return null; michael@0: } michael@0: return actor.rawNode; michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * Returned from any call that might return a node that isn't connected to root by michael@0: * nodes the child has seen, such as querySelector. michael@0: */ michael@0: types.addDictType("disconnectedNode", { michael@0: // The actual node to return michael@0: node: "domnode", michael@0: michael@0: // Nodes that are needed to connect the node to a node the client has already seen michael@0: newParents: "array:domnode" michael@0: }); michael@0: michael@0: types.addDictType("disconnectedNodeArray", { michael@0: // The actual node list to return michael@0: nodes: "array:domnode", michael@0: michael@0: // Nodes that are needed to connect those nodes to the root. michael@0: newParents: "array:domnode" michael@0: }); michael@0: michael@0: types.addDictType("dommutation", {}); michael@0: michael@0: /** michael@0: * Server side of a node list as returned by querySelectorAll() michael@0: */ michael@0: var NodeListActor = exports.NodeListActor = protocol.ActorClass({ michael@0: typeName: "domnodelist", michael@0: michael@0: initialize: function(walker, nodeList) { michael@0: protocol.Actor.prototype.initialize.call(this); michael@0: this.walker = walker; michael@0: this.nodeList = nodeList; michael@0: }, michael@0: michael@0: destroy: function() { michael@0: protocol.Actor.prototype.destroy.call(this); michael@0: }, michael@0: michael@0: /** michael@0: * Instead of storing a connection object, the NodeActor gets its connection michael@0: * from its associated walker. michael@0: */ michael@0: get conn() { michael@0: return this.walker.conn; michael@0: }, michael@0: michael@0: /** michael@0: * Items returned by this actor should belong to the parent walker. michael@0: */ michael@0: marshallPool: function() { michael@0: return this.walker; michael@0: }, michael@0: michael@0: // Returns the JSON representation of this object over the wire. michael@0: form: function() { michael@0: return { michael@0: actor: this.actorID, michael@0: length: this.nodeList.length michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Get a single node from the node list. michael@0: */ michael@0: item: method(function(index) { michael@0: return this.walker.attachElement(this.nodeList[index]); michael@0: }, { michael@0: request: { item: Arg(0) }, michael@0: response: RetVal("disconnectedNode") michael@0: }), michael@0: michael@0: /** michael@0: * Get a range of the items from the node list. michael@0: */ michael@0: items: method(function(start=0, end=this.nodeList.length) { michael@0: let items = [this.walker._ref(item) for (item of Array.prototype.slice.call(this.nodeList, start, end))]; michael@0: let newParents = new Set(); michael@0: for (let item of items) { michael@0: this.walker.ensurePathToRoot(item, newParents); michael@0: } michael@0: return { michael@0: nodes: items, michael@0: newParents: [node for (node of newParents)] michael@0: } michael@0: }, { michael@0: request: { michael@0: start: Arg(0, "nullable:number"), michael@0: end: Arg(1, "nullable:number") michael@0: }, michael@0: response: RetVal("disconnectedNodeArray") michael@0: }), michael@0: michael@0: release: method(function() {}, { release: true }) michael@0: }); michael@0: michael@0: /** michael@0: * Client side of a node list as returned by querySelectorAll() michael@0: */ michael@0: var NodeListFront = exports.NodeListFront = protocol.FrontClass(NodeListActor, { michael@0: initialize: function(client, form) { michael@0: protocol.Front.prototype.initialize.call(this, client, form); michael@0: }, michael@0: michael@0: destroy: function() { michael@0: protocol.Front.prototype.destroy.call(this); michael@0: }, michael@0: michael@0: marshallPool: function() { michael@0: return this.parent(); michael@0: }, michael@0: michael@0: // Update the object given a form representation off the wire. michael@0: form: function(json) { michael@0: this.length = json.length; michael@0: }, michael@0: michael@0: item: protocol.custom(function(index) { michael@0: return this._item(index).then(response => { michael@0: return response.node; michael@0: }); michael@0: }, { michael@0: impl: "_item" michael@0: }), michael@0: michael@0: items: protocol.custom(function(start, end) { michael@0: return this._items(start, end).then(response => { michael@0: return response.nodes; michael@0: }); michael@0: }, { michael@0: impl: "_items" michael@0: }) michael@0: }); michael@0: michael@0: // Some common request/response templates for the dom walker michael@0: michael@0: let nodeArrayMethod = { michael@0: request: { michael@0: node: Arg(0, "domnode"), michael@0: maxNodes: Option(1), michael@0: center: Option(1, "domnode"), michael@0: start: Option(1, "domnode"), michael@0: whatToShow: Option(1) michael@0: }, michael@0: response: RetVal(types.addDictType("domtraversalarray", { michael@0: nodes: "array:domnode" michael@0: })) michael@0: }; michael@0: michael@0: let traversalMethod = { michael@0: request: { michael@0: node: Arg(0, "domnode"), michael@0: whatToShow: Option(1) michael@0: }, michael@0: response: { michael@0: node: RetVal("nullable:domnode") michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Server side of the DOM walker. michael@0: */ michael@0: var WalkerActor = protocol.ActorClass({ michael@0: typeName: "domwalker", michael@0: michael@0: events: { michael@0: "new-mutations" : { michael@0: type: "newMutations" michael@0: }, michael@0: "picker-node-picked" : { michael@0: type: "pickerNodePicked", michael@0: node: Arg(0, "disconnectedNode") michael@0: }, michael@0: "picker-node-hovered" : { michael@0: type: "pickerNodeHovered", michael@0: node: Arg(0, "disconnectedNode") michael@0: }, michael@0: "highlighter-ready" : { michael@0: type: "highlighter-ready" michael@0: }, michael@0: "highlighter-hide" : { michael@0: type: "highlighter-hide" michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Create the WalkerActor michael@0: * @param DebuggerServerConnection conn michael@0: * The server connection. michael@0: */ michael@0: initialize: function(conn, tabActor, options) { michael@0: protocol.Actor.prototype.initialize.call(this, conn); michael@0: this.tabActor = tabActor; michael@0: this.rootWin = tabActor.window; michael@0: this.rootDoc = this.rootWin.document; michael@0: this._refMap = new Map(); michael@0: this._pendingMutations = []; michael@0: this._activePseudoClassLocks = new Set(); michael@0: michael@0: this.layoutHelpers = new LayoutHelpers(this.rootWin); michael@0: michael@0: // Nodes which have been removed from the client's known michael@0: // ownership tree are considered "orphaned", and stored in michael@0: // this set. michael@0: this._orphaned = new Set(); michael@0: michael@0: // The client can tell the walker that it is interested in a node michael@0: // even when it is orphaned with the `retainNode` method. This michael@0: // list contains orphaned nodes that were so retained. michael@0: this._retainedOrphans = new Set(); michael@0: michael@0: this.onMutations = this.onMutations.bind(this); michael@0: this.onFrameLoad = this.onFrameLoad.bind(this); michael@0: this.onFrameUnload = this.onFrameUnload.bind(this); michael@0: michael@0: events.on(tabActor, "will-navigate", this.onFrameUnload); michael@0: events.on(tabActor, "navigate", this.onFrameLoad); michael@0: michael@0: // Ensure that the root document node actor is ready and michael@0: // managed. michael@0: this.rootNode = this.document(); michael@0: }, michael@0: michael@0: // Returns the JSON representation of this object over the wire. michael@0: form: function() { michael@0: return { michael@0: actor: this.actorID, michael@0: root: this.rootNode.form() michael@0: } michael@0: }, michael@0: michael@0: toString: function() { michael@0: return "[WalkerActor " + this.actorID + "]"; michael@0: }, michael@0: michael@0: destroy: function() { michael@0: this._hoveredNode = null; michael@0: this.clearPseudoClassLocks(); michael@0: this._activePseudoClassLocks = null; michael@0: this.rootDoc = null; michael@0: events.emit(this, "destroyed"); michael@0: protocol.Actor.prototype.destroy.call(this); michael@0: }, michael@0: michael@0: release: method(function() {}, { release: true }), michael@0: michael@0: unmanage: function(actor) { michael@0: if (actor instanceof NodeActor) { michael@0: if (this._activePseudoClassLocks && michael@0: this._activePseudoClassLocks.has(actor)) { michael@0: this.clearPsuedoClassLocks(actor); michael@0: } michael@0: this._refMap.delete(actor.rawNode); michael@0: } michael@0: protocol.Actor.prototype.unmanage.call(this, actor); michael@0: }, michael@0: michael@0: _ref: function(node) { michael@0: let actor = this._refMap.get(node); michael@0: if (actor) return actor; michael@0: michael@0: actor = new NodeActor(this, node); michael@0: michael@0: // Add the node actor as a child of this walker actor, assigning michael@0: // it an actorID. michael@0: this.manage(actor); michael@0: this._refMap.set(node, actor); michael@0: michael@0: if (node.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) { michael@0: this._watchDocument(actor); michael@0: } michael@0: return actor; michael@0: }, michael@0: michael@0: /** michael@0: * This is kept for backward-compatibility reasons with older remote targets. michael@0: * Targets prior to bug 916443. michael@0: * michael@0: * pick/cancelPick are used to pick a node on click on the content michael@0: * document. But in their implementation prior to bug 916443, they don't allow michael@0: * highlighting on hover. michael@0: * The client-side now uses the highlighter actor's pick and cancelPick michael@0: * methods instead. The client-side uses the the highlightable trait found in michael@0: * the root actor to determine which version of pick to use. michael@0: * michael@0: * As for highlight, the new highlighter actor is used instead of the walker's michael@0: * highlight method. Same here though, the client-side uses the highlightable michael@0: * trait to dertermine which to use. michael@0: * michael@0: * Keeping these actor methods for now allows newer client-side debuggers to michael@0: * inspect fxos 1.2 remote targets or older firefox desktop remote targets. michael@0: */ michael@0: pick: method(function() {}, {request: {}, response: RetVal("disconnectedNode")}), michael@0: cancelPick: method(function() {}), michael@0: highlight: method(function(node) {}, {request: {node: Arg(0, "nullable:domnode")}}), michael@0: michael@0: attachElement: function(node) { michael@0: node = this._ref(node); michael@0: let newParents = this.ensurePathToRoot(node); michael@0: return { michael@0: node: node, michael@0: newParents: [parent for (parent of newParents)] michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Watch the given document node for mutations using the DOM observer michael@0: * API. michael@0: */ michael@0: _watchDocument: function(actor) { michael@0: let node = actor.rawNode; michael@0: // Create the observer on the node's actor. The node will make sure michael@0: // the observer is cleaned up when the actor is released. michael@0: actor.observer = new actor.rawNode.defaultView.MutationObserver(this.onMutations); michael@0: actor.observer.observe(node, { michael@0: attributes: true, michael@0: characterData: true, michael@0: childList: true, michael@0: subtree: true michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Return the document node that contains the given node, michael@0: * or the root node if no node is specified. michael@0: * @param NodeActor node michael@0: * The node whose document is needed, or null to michael@0: * return the root. michael@0: */ michael@0: document: method(function(node) { michael@0: let doc = node ? nodeDocument(node.rawNode) : this.rootDoc; michael@0: return this._ref(doc); michael@0: }, { michael@0: request: { node: Arg(0, "nullable:domnode") }, michael@0: response: { node: RetVal("domnode") }, michael@0: }), michael@0: michael@0: /** michael@0: * Return the documentElement for the document containing the michael@0: * given node. michael@0: * @param NodeActor node michael@0: * The node whose documentElement is requested, or null michael@0: * to use the root document. michael@0: */ michael@0: documentElement: method(function(node) { michael@0: let elt = node ? nodeDocument(node.rawNode).documentElement : this.rootDoc.documentElement; michael@0: return this._ref(elt); michael@0: }, { michael@0: request: { node: Arg(0, "nullable:domnode") }, michael@0: response: { node: RetVal("domnode") }, michael@0: }), michael@0: michael@0: /** michael@0: * Return all parents of the given node, ordered from immediate parent michael@0: * to root. michael@0: * @param NodeActor node michael@0: * The node whose parents are requested. michael@0: * @param object options michael@0: * Named options, including: michael@0: * `sameDocument`: If true, parents will be restricted to the same michael@0: * document as the node. michael@0: */ michael@0: parents: method(function(node, options={}) { michael@0: let walker = documentWalker(node.rawNode, this.rootWin); michael@0: let parents = []; michael@0: let cur; michael@0: while((cur = walker.parentNode())) { michael@0: if (options.sameDocument && cur.ownerDocument != node.rawNode.ownerDocument) { michael@0: break; michael@0: } michael@0: parents.push(this._ref(cur)); michael@0: } michael@0: return parents; michael@0: }, { michael@0: request: { michael@0: node: Arg(0, "domnode"), michael@0: sameDocument: Option(1) michael@0: }, michael@0: response: { michael@0: nodes: RetVal("array:domnode") michael@0: }, michael@0: }), michael@0: michael@0: parentNode: function(node) { michael@0: let walker = documentWalker(node.rawNode, this.rootWin); michael@0: let parent = walker.parentNode(); michael@0: if (parent) { michael@0: return this._ref(parent); michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * Mark a node as 'retained'. michael@0: * michael@0: * A retained node is not released when `releaseNode` is called on its michael@0: * parent, or when a parent is released with the `cleanup` option to michael@0: * `getMutations`. michael@0: * michael@0: * When a retained node's parent is released, a retained mode is added to michael@0: * the walker's "retained orphans" list. michael@0: * michael@0: * Retained nodes can be deleted by providing the `force` option to michael@0: * `releaseNode`. They will also be released when their document michael@0: * has been destroyed. michael@0: * michael@0: * Retaining a node makes no promise about its children; They can michael@0: * still be removed by normal means. michael@0: */ michael@0: retainNode: method(function(node) { michael@0: node.retained = true; michael@0: }, { michael@0: request: { node: Arg(0, "domnode") }, michael@0: response: {} michael@0: }), michael@0: michael@0: /** michael@0: * Remove the 'retained' mark from a node. If the node was a michael@0: * retained orphan, release it. michael@0: */ michael@0: unretainNode: method(function(node) { michael@0: node.retained = false; michael@0: if (this._retainedOrphans.has(node)) { michael@0: this._retainedOrphans.delete(node); michael@0: this.releaseNode(node); michael@0: } michael@0: }, { michael@0: request: { node: Arg(0, "domnode") }, michael@0: response: {}, michael@0: }), michael@0: michael@0: /** michael@0: * Release actors for a node and all child nodes. michael@0: */ michael@0: releaseNode: method(function(node, options={}) { michael@0: if (node.retained && !options.force) { michael@0: this._retainedOrphans.add(node); michael@0: return; michael@0: } michael@0: michael@0: if (node.retained) { michael@0: // Forcing a retained node to go away. michael@0: this._retainedOrphans.delete(node); michael@0: } michael@0: michael@0: let walker = documentWalker(node.rawNode, this.rootWin); michael@0: michael@0: let child = walker.firstChild(); michael@0: while (child) { michael@0: let childActor = this._refMap.get(child); michael@0: if (childActor) { michael@0: this.releaseNode(childActor, options); michael@0: } michael@0: child = walker.nextSibling(); michael@0: } michael@0: michael@0: node.destroy(); michael@0: }, { michael@0: request: { michael@0: node: Arg(0, "domnode"), michael@0: force: Option(1) michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Add any nodes between `node` and the walker's root node that have not michael@0: * yet been seen by the client. michael@0: */ michael@0: ensurePathToRoot: function(node, newParents=new Set()) { michael@0: if (!node) { michael@0: return newParents; michael@0: } michael@0: let walker = documentWalker(node.rawNode, this.rootWin); michael@0: let cur; michael@0: while ((cur = walker.parentNode())) { michael@0: let parent = this._refMap.get(cur); michael@0: if (!parent) { michael@0: // This parent didn't exist, so hasn't been seen by the client yet. michael@0: newParents.add(this._ref(cur)); michael@0: } else { michael@0: // This parent did exist, so the client knows about it. michael@0: return newParents; michael@0: } michael@0: } michael@0: return newParents; michael@0: }, michael@0: michael@0: /** michael@0: * Return children of the given node. By default this method will return michael@0: * all children of the node, but there are options that can restrict this michael@0: * to a more manageable subset. michael@0: * michael@0: * @param NodeActor node michael@0: * The node whose children you're curious about. michael@0: * @param object options michael@0: * Named options: michael@0: * `maxNodes`: The set of nodes returned by the method will be no longer michael@0: * than maxNodes. michael@0: * `start`: If a node is specified, the list of nodes will start michael@0: * with the given child. Mutally exclusive with `center`. michael@0: * `center`: If a node is specified, the given node will be as centered michael@0: * as possible in the list, given how close to the ends of the child michael@0: * list it is. Mutually exclusive with `start`. michael@0: * `whatToShow`: A bitmask of node types that should be included. See michael@0: * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter. michael@0: * michael@0: * @returns an object with three items: michael@0: * hasFirst: true if the first child of the node is included in the list. michael@0: * hasLast: true if the last child of the node is included in the list. michael@0: * nodes: Child nodes returned by the request. michael@0: */ michael@0: children: method(function(node, options={}) { michael@0: if (options.center && options.start) { michael@0: throw Error("Can't specify both 'center' and 'start' options."); michael@0: } michael@0: let maxNodes = options.maxNodes || -1; michael@0: if (maxNodes == -1) { michael@0: maxNodes = Number.MAX_VALUE; michael@0: } michael@0: michael@0: // We're going to create a few document walkers with the same filter, michael@0: // make it easier. michael@0: let filteredWalker = (node) => { michael@0: return documentWalker(node, this.rootWin, options.whatToShow); michael@0: }; michael@0: michael@0: // Need to know the first and last child. michael@0: let rawNode = node.rawNode; michael@0: let firstChild = filteredWalker(rawNode).firstChild(); michael@0: let lastChild = filteredWalker(rawNode).lastChild(); michael@0: michael@0: if (!firstChild) { michael@0: // No children, we're done. michael@0: return { hasFirst: true, hasLast: true, nodes: [] }; michael@0: } michael@0: michael@0: let start; michael@0: if (options.center) { michael@0: start = options.center.rawNode; michael@0: } else if (options.start) { michael@0: start = options.start.rawNode; michael@0: } else { michael@0: start = firstChild; michael@0: } michael@0: michael@0: let nodes = []; michael@0: michael@0: // Start by reading backward from the starting point if we're centering... michael@0: let backwardWalker = filteredWalker(start); michael@0: if (start != firstChild && options.center) { michael@0: backwardWalker.previousSibling(); michael@0: let backwardCount = Math.floor(maxNodes / 2); michael@0: let backwardNodes = this._readBackward(backwardWalker, backwardCount); michael@0: nodes = backwardNodes; michael@0: } michael@0: michael@0: // Then read forward by any slack left in the max children... michael@0: let forwardWalker = filteredWalker(start); michael@0: let forwardCount = maxNodes - nodes.length; michael@0: nodes = nodes.concat(this._readForward(forwardWalker, forwardCount)); michael@0: michael@0: // If there's any room left, it means we've run all the way to the end. michael@0: // If we're centering, check if there are more items to read at the front. michael@0: let remaining = maxNodes - nodes.length; michael@0: if (options.center && remaining > 0 && nodes[0].rawNode != firstChild) { michael@0: let firstNodes = this._readBackward(backwardWalker, remaining); michael@0: michael@0: // Then put it all back together. michael@0: nodes = firstNodes.concat(nodes); michael@0: } michael@0: michael@0: return { michael@0: hasFirst: nodes[0].rawNode == firstChild, michael@0: hasLast: nodes[nodes.length - 1].rawNode == lastChild, michael@0: nodes: nodes michael@0: }; michael@0: }, nodeArrayMethod), michael@0: michael@0: /** michael@0: * Return siblings of the given node. By default this method will return michael@0: * all siblings of the node, but there are options that can restrict this michael@0: * to a more manageable subset. michael@0: * michael@0: * If `start` or `center` are not specified, this method will center on the michael@0: * node whose siblings are requested. michael@0: * michael@0: * @param NodeActor node michael@0: * The node whose children you're curious about. michael@0: * @param object options michael@0: * Named options: michael@0: * `maxNodes`: The set of nodes returned by the method will be no longer michael@0: * than maxNodes. michael@0: * `start`: If a node is specified, the list of nodes will start michael@0: * with the given child. Mutally exclusive with `center`. michael@0: * `center`: If a node is specified, the given node will be as centered michael@0: * as possible in the list, given how close to the ends of the child michael@0: * list it is. Mutually exclusive with `start`. michael@0: * `whatToShow`: A bitmask of node types that should be included. See michael@0: * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter. michael@0: * michael@0: * @returns an object with three items: michael@0: * hasFirst: true if the first child of the node is included in the list. michael@0: * hasLast: true if the last child of the node is included in the list. michael@0: * nodes: Child nodes returned by the request. michael@0: */ michael@0: siblings: method(function(node, options={}) { michael@0: let parentNode = documentWalker(node.rawNode, this.rootWin).parentNode(); michael@0: if (!parentNode) { michael@0: return { michael@0: hasFirst: true, michael@0: hasLast: true, michael@0: nodes: [node] michael@0: }; michael@0: } michael@0: michael@0: if (!(options.start || options.center)) { michael@0: options.center = node; michael@0: } michael@0: michael@0: return this.children(this._ref(parentNode), options); michael@0: }, nodeArrayMethod), michael@0: michael@0: /** michael@0: * Get the next sibling of a given node. Getting nodes one at a time michael@0: * might be inefficient, be careful. michael@0: * michael@0: * @param object options michael@0: * Named options: michael@0: * `whatToShow`: A bitmask of node types that should be included. See michael@0: * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter. michael@0: */ michael@0: nextSibling: method(function(node, options={}) { michael@0: let walker = documentWalker(node.rawNode, this.rootWin, options.whatToShow || Ci.nsIDOMNodeFilter.SHOW_ALL); michael@0: let sibling = walker.nextSibling(); michael@0: return sibling ? this._ref(sibling) : null; michael@0: }, traversalMethod), michael@0: michael@0: /** michael@0: * Get the previous sibling of a given node. Getting nodes one at a time michael@0: * might be inefficient, be careful. michael@0: * michael@0: * @param object options michael@0: * Named options: michael@0: * `whatToShow`: A bitmask of node types that should be included. See michael@0: * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter. michael@0: */ michael@0: previousSibling: method(function(node, options={}) { michael@0: let walker = documentWalker(node.rawNode, this.rootWin, options.whatToShow || Ci.nsIDOMNodeFilter.SHOW_ALL); michael@0: let sibling = walker.previousSibling(); michael@0: return sibling ? this._ref(sibling) : null; michael@0: }, traversalMethod), michael@0: michael@0: /** michael@0: * Helper function for the `children` method: Read forward in the sibling michael@0: * list into an array with `count` items, including the current node. michael@0: */ michael@0: _readForward: function(walker, count) { michael@0: let ret = []; michael@0: let node = walker.currentNode; michael@0: do { michael@0: ret.push(this._ref(node)); michael@0: node = walker.nextSibling(); michael@0: } while (node && --count); michael@0: return ret; michael@0: }, michael@0: michael@0: /** michael@0: * Helper function for the `children` method: Read backward in the sibling michael@0: * list into an array with `count` items, including the current node. michael@0: */ michael@0: _readBackward: function(walker, count) { michael@0: let ret = []; michael@0: let node = walker.currentNode; michael@0: do { michael@0: ret.push(this._ref(node)); michael@0: node = walker.previousSibling(); michael@0: } while(node && --count); michael@0: ret.reverse(); michael@0: return ret; michael@0: }, michael@0: michael@0: /** michael@0: * Return the first node in the document that matches the given selector. michael@0: * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelector michael@0: * michael@0: * @param NodeActor baseNode michael@0: * @param string selector michael@0: */ michael@0: querySelector: method(function(baseNode, selector) { michael@0: if (!baseNode) { michael@0: return {} michael@0: }; michael@0: let node = baseNode.rawNode.querySelector(selector); michael@0: michael@0: if (!node) { michael@0: return {} michael@0: }; michael@0: michael@0: return this.attachElement(node); michael@0: }, { michael@0: request: { michael@0: node: Arg(0, "domnode"), michael@0: selector: Arg(1) michael@0: }, michael@0: response: RetVal("disconnectedNode") michael@0: }), michael@0: michael@0: /** michael@0: * Return a NodeListActor with all nodes that match the given selector. michael@0: * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelectorAll michael@0: * michael@0: * @param NodeActor baseNode michael@0: * @param string selector michael@0: */ michael@0: querySelectorAll: method(function(baseNode, selector) { michael@0: let nodeList = null; michael@0: michael@0: try { michael@0: nodeList = baseNode.rawNode.querySelectorAll(selector); michael@0: } catch(e) { michael@0: // Bad selector. Do nothing as the selector can come from a searchbox. michael@0: } michael@0: michael@0: return new NodeListActor(this, nodeList); michael@0: }, { michael@0: request: { michael@0: node: Arg(0, "domnode"), michael@0: selector: Arg(1) michael@0: }, michael@0: response: { michael@0: list: RetVal("domnodelist") michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Returns a list of matching results for CSS selector autocompletion. michael@0: * michael@0: * @param string query michael@0: * The selector query being completed michael@0: * @param string completing michael@0: * The exact token being completed out of the query michael@0: * @param string selectorState michael@0: * One of "pseudo", "id", "tag", "class", "null" michael@0: */ michael@0: getSuggestionsForQuery: method(function(query, completing, selectorState) { michael@0: let sugs = { michael@0: classes: new Map, michael@0: tags: new Map michael@0: }; michael@0: let result = []; michael@0: let nodes = null; michael@0: // Filtering and sorting the results so that protocol transfer is miminal. michael@0: switch (selectorState) { michael@0: case "pseudo": michael@0: result = PSEUDO_SELECTORS.filter(item => { michael@0: return item[0].startsWith(":" + completing); michael@0: }); michael@0: break; michael@0: michael@0: case "class": michael@0: if (!query) { michael@0: nodes = this.rootDoc.querySelectorAll("[class]"); michael@0: } michael@0: else { michael@0: nodes = this.rootDoc.querySelectorAll(query); michael@0: } michael@0: for (let node of nodes) { michael@0: for (let className of node.className.split(" ")) { michael@0: sugs.classes.set(className, (sugs.classes.get(className)|0) + 1); michael@0: } michael@0: } michael@0: sugs.classes.delete(""); michael@0: // Editing the style editor may make the stylesheet have errors and michael@0: // thus the page's elements' styles start changing with a transition. michael@0: // That transition comes from the `moz-styleeditor-transitioning` class. michael@0: sugs.classes.delete("moz-styleeditor-transitioning"); michael@0: sugs.classes.delete(HIDDEN_CLASS); michael@0: for (let [className, count] of sugs.classes) { michael@0: if (className.startsWith(completing)) { michael@0: result.push(["." + className, count]); michael@0: } michael@0: } michael@0: break; michael@0: michael@0: case "id": michael@0: if (!query) { michael@0: nodes = this.rootDoc.querySelectorAll("[id]"); michael@0: } michael@0: else { michael@0: nodes = this.rootDoc.querySelectorAll(query); michael@0: } michael@0: for (let node of nodes) { michael@0: if (node.id.startsWith(completing)) { michael@0: result.push(["#" + node.id, 1]); michael@0: } michael@0: } michael@0: break; michael@0: michael@0: case "tag": michael@0: if (!query) { michael@0: nodes = this.rootDoc.getElementsByTagName("*"); michael@0: } michael@0: else { michael@0: nodes = this.rootDoc.querySelectorAll(query); michael@0: } michael@0: for (let node of nodes) { michael@0: let tag = node.tagName.toLowerCase(); michael@0: sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1); michael@0: } michael@0: for (let [tag, count] of sugs.tags) { michael@0: if ((new RegExp("^" + completing + ".*", "i")).test(tag)) { michael@0: result.push([tag, count]); michael@0: } michael@0: } michael@0: break; michael@0: michael@0: case "null": michael@0: nodes = this.rootDoc.querySelectorAll(query); michael@0: for (let node of nodes) { michael@0: node.id && result.push(["#" + node.id, 1]); michael@0: let tag = node.tagName.toLowerCase(); michael@0: sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1); michael@0: for (let className of node.className.split(" ")) { michael@0: sugs.classes.set(className, (sugs.classes.get(className)|0) + 1); michael@0: } michael@0: } michael@0: for (let [tag, count] of sugs.tags) { michael@0: tag && result.push([tag, count]); michael@0: } michael@0: sugs.classes.delete(""); michael@0: // Editing the style editor may make the stylesheet have errors and michael@0: // thus the page's elements' styles start changing with a transition. michael@0: // That transition comes from the `moz-styleeditor-transitioning` class. michael@0: sugs.classes.delete("moz-styleeditor-transitioning"); michael@0: sugs.classes.delete(HIDDEN_CLASS); michael@0: for (let [className, count] of sugs.classes) { michael@0: className && result.push(["." + className, count]); michael@0: } michael@0: } michael@0: michael@0: // Sort alphabetically in increaseing order. michael@0: result = result.sort(); michael@0: // Sort based on count in decreasing order. michael@0: result = result.sort(function(a, b) { michael@0: return b[1] - a[1]; michael@0: }); michael@0: michael@0: result.slice(0, 25); michael@0: michael@0: return { michael@0: query: query, michael@0: suggestions: result michael@0: }; michael@0: }, { michael@0: request: { michael@0: query: Arg(0), michael@0: completing: Arg(1), michael@0: selectorState: Arg(2) michael@0: }, michael@0: response: { michael@0: list: RetVal("array:array:string") michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Add a pseudo-class lock to a node. michael@0: * michael@0: * @param NodeActor node michael@0: * @param string pseudo michael@0: * A pseudoclass: ':hover', ':active', ':focus' michael@0: * @param options michael@0: * Options object: michael@0: * `parents`: True if the pseudo-class should be added michael@0: * to parent nodes. michael@0: * michael@0: * @returns An empty packet. A "pseudoClassLock" mutation will michael@0: * be queued for any changed nodes. michael@0: */ michael@0: addPseudoClassLock: method(function(node, pseudo, options={}) { michael@0: this._addPseudoClassLock(node, pseudo); michael@0: michael@0: if (!options.parents) { michael@0: return; michael@0: } michael@0: michael@0: let walker = documentWalker(node.rawNode, this.rootWin); michael@0: let cur; michael@0: while ((cur = walker.parentNode())) { michael@0: let curNode = this._ref(cur); michael@0: this._addPseudoClassLock(curNode, pseudo); michael@0: } michael@0: }, { michael@0: request: { michael@0: node: Arg(0, "domnode"), michael@0: pseudoClass: Arg(1), michael@0: parents: Option(2) michael@0: }, michael@0: response: {} michael@0: }), michael@0: michael@0: _queuePseudoClassMutation: function(node) { michael@0: this.queueMutation({ michael@0: target: node.actorID, michael@0: type: "pseudoClassLock", michael@0: pseudoClassLocks: node.writePseudoClassLocks() michael@0: }); michael@0: }, michael@0: michael@0: _addPseudoClassLock: function(node, pseudo) { michael@0: if (node.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) { michael@0: return false; michael@0: } michael@0: DOMUtils.addPseudoClassLock(node.rawNode, pseudo); michael@0: this._activePseudoClassLocks.add(node); michael@0: this._queuePseudoClassMutation(node); michael@0: return true; michael@0: }, michael@0: michael@0: _installHelperSheet: function(node) { michael@0: if (!this.installedHelpers) { michael@0: this.installedHelpers = new WeakMap; michael@0: } michael@0: let win = node.rawNode.ownerDocument.defaultView; michael@0: if (!this.installedHelpers.has(win)) { michael@0: let { Style } = require("sdk/stylesheet/style"); michael@0: let { attach } = require("sdk/content/mod"); michael@0: let style = Style({source: HELPER_SHEET, type: "agent" }); michael@0: attach(style, win); michael@0: this.installedHelpers.set(win, style); michael@0: } michael@0: }, michael@0: michael@0: hideNode: method(function(node) { michael@0: this._installHelperSheet(node); michael@0: node.rawNode.classList.add(HIDDEN_CLASS); michael@0: }, { michael@0: request: { node: Arg(0, "domnode") } michael@0: }), michael@0: michael@0: unhideNode: method(function(node) { michael@0: node.rawNode.classList.remove(HIDDEN_CLASS); michael@0: }, { michael@0: request: { node: Arg(0, "domnode") } michael@0: }), michael@0: michael@0: /** michael@0: * Remove a pseudo-class lock from a node. michael@0: * michael@0: * @param NodeActor node michael@0: * @param string pseudo michael@0: * A pseudoclass: ':hover', ':active', ':focus' michael@0: * @param options michael@0: * Options object: michael@0: * `parents`: True if the pseudo-class should be removed michael@0: * from parent nodes. michael@0: * michael@0: * @returns An empty response. "pseudoClassLock" mutations michael@0: * will be emitted for any changed nodes. michael@0: */ michael@0: removePseudoClassLock: method(function(node, pseudo, options={}) { michael@0: this._removePseudoClassLock(node, pseudo); michael@0: michael@0: if (!options.parents) { michael@0: return; michael@0: } michael@0: michael@0: let walker = documentWalker(node.rawNode, this.rootWin); michael@0: let cur; michael@0: while ((cur = walker.parentNode())) { michael@0: let curNode = this._ref(cur); michael@0: this._removePseudoClassLock(curNode, pseudo); michael@0: } michael@0: }, { michael@0: request: { michael@0: node: Arg(0, "domnode"), michael@0: pseudoClass: Arg(1), michael@0: parents: Option(2) michael@0: }, michael@0: response: {} michael@0: }), michael@0: michael@0: _removePseudoClassLock: function(node, pseudo) { michael@0: if (node.rawNode.nodeType != Ci.nsIDOMNode.ELEMENT_NODE) { michael@0: return false; michael@0: } michael@0: DOMUtils.removePseudoClassLock(node.rawNode, pseudo); michael@0: if (!node.writePseudoClassLocks()) { michael@0: this._activePseudoClassLocks.delete(node); michael@0: } michael@0: this._queuePseudoClassMutation(node); michael@0: return true; michael@0: }, michael@0: michael@0: /** michael@0: * Clear all the pseudo-classes on a given node michael@0: * or all nodes. michael@0: */ michael@0: clearPseudoClassLocks: method(function(node) { michael@0: if (node) { michael@0: DOMUtils.clearPseudoClassLocks(node.rawNode); michael@0: this._activePseudoClassLocks.delete(node); michael@0: this._queuePseudoClassMutation(node); michael@0: } else { michael@0: for (let locked of this._activePseudoClassLocks) { michael@0: DOMUtils.clearPseudoClassLocks(locked.rawNode); michael@0: this._activePseudoClassLocks.delete(locked); michael@0: this._queuePseudoClassMutation(locked); michael@0: } michael@0: } michael@0: }, { michael@0: request: { michael@0: node: Arg(0, "nullable:domnode") michael@0: }, michael@0: response: {} michael@0: }), michael@0: michael@0: /** michael@0: * Get a node's innerHTML property. michael@0: */ michael@0: innerHTML: method(function(node) { michael@0: return LongStringActor(this.conn, node.rawNode.innerHTML); michael@0: }, { michael@0: request: { michael@0: node: Arg(0, "domnode") michael@0: }, michael@0: response: { michael@0: value: RetVal("longstring") michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Get a node's outerHTML property. michael@0: */ michael@0: outerHTML: method(function(node) { michael@0: return LongStringActor(this.conn, node.rawNode.outerHTML); michael@0: }, { michael@0: request: { michael@0: node: Arg(0, "domnode") michael@0: }, michael@0: response: { michael@0: value: RetVal("longstring") michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Set a node's outerHTML property. michael@0: */ michael@0: setOuterHTML: method(function(node, value) { michael@0: let parsedDOM = DOMParser.parseFromString(value, "text/html"); michael@0: let rawNode = node.rawNode; michael@0: let parentNode = rawNode.parentNode; michael@0: michael@0: // Special case for head and body. Setting document.body.outerHTML michael@0: // creates an extra tag, and document.head.outerHTML creates michael@0: // an extra . So instead we will call replaceChild with the michael@0: // parsed DOM, assuming that they aren't trying to set both tags at once. michael@0: if (rawNode.tagName === "BODY") { michael@0: if (parsedDOM.head.innerHTML === "") { michael@0: parentNode.replaceChild(parsedDOM.body, rawNode); michael@0: } else { michael@0: rawNode.outerHTML = value; michael@0: } michael@0: } else if (rawNode.tagName === "HEAD") { michael@0: if (parsedDOM.body.innerHTML === "") { michael@0: parentNode.replaceChild(parsedDOM.head, rawNode); michael@0: } else { michael@0: rawNode.outerHTML = value; michael@0: } michael@0: } else if (node.isDocumentElement()) { michael@0: // Unable to set outerHTML on the document element. Fall back by michael@0: // setting attributes manually, then replace the body and head elements. michael@0: let finalAttributeModifications = []; michael@0: let attributeModifications = {}; michael@0: for (let attribute of rawNode.attributes) { michael@0: attributeModifications[attribute.name] = null; michael@0: } michael@0: for (let attribute of parsedDOM.documentElement.attributes) { michael@0: attributeModifications[attribute.name] = attribute.value; michael@0: } michael@0: for (let key in attributeModifications) { michael@0: finalAttributeModifications.push({ michael@0: attributeName: key, michael@0: newValue: attributeModifications[key] michael@0: }); michael@0: } michael@0: node.modifyAttributes(finalAttributeModifications); michael@0: rawNode.replaceChild(parsedDOM.head, rawNode.querySelector("head")); michael@0: rawNode.replaceChild(parsedDOM.body, rawNode.querySelector("body")); michael@0: } else { michael@0: rawNode.outerHTML = value; michael@0: } michael@0: }, { michael@0: request: { michael@0: node: Arg(0, "domnode"), michael@0: value: Arg(1), michael@0: }, michael@0: response: { michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Removes a node from its parent node. michael@0: * michael@0: * @returns The node's nextSibling before it was removed. michael@0: */ michael@0: removeNode: method(function(node) { michael@0: if ((node.rawNode.ownerDocument && michael@0: node.rawNode.ownerDocument.documentElement === this.rawNode) || michael@0: node.rawNode.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) { michael@0: throw Error("Cannot remove document or document elements."); michael@0: } michael@0: let nextSibling = this.nextSibling(node); michael@0: if (node.rawNode.parentNode) { michael@0: node.rawNode.parentNode.removeChild(node.rawNode); michael@0: // Mutation events will take care of the rest. michael@0: } michael@0: return nextSibling; michael@0: }, { michael@0: request: { michael@0: node: Arg(0, "domnode") michael@0: }, michael@0: response: { michael@0: nextSibling: RetVal("nullable:domnode") michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Insert a node into the DOM. michael@0: */ michael@0: insertBefore: method(function(node, parent, sibling) { michael@0: parent.rawNode.insertBefore(node.rawNode, sibling ? sibling.rawNode : null); michael@0: }, { michael@0: request: { michael@0: node: Arg(0, "domnode"), michael@0: parent: Arg(1, "domnode"), michael@0: sibling: Arg(2, "nullable:domnode") michael@0: }, michael@0: response: {} michael@0: }), michael@0: michael@0: /** michael@0: * Get any pending mutation records. Must be called by the client after michael@0: * the `new-mutations` notification is received. Returns an array of michael@0: * mutation records. michael@0: * michael@0: * Mutation records have a basic structure: michael@0: * michael@0: * { michael@0: * type: attributes|characterData|childList, michael@0: * target: , michael@0: * } michael@0: * michael@0: * And additional attributes based on the mutation type: michael@0: * michael@0: * `attributes` type: michael@0: * attributeName: - the attribute that changed michael@0: * attributeNamespace: - the attribute's namespace URI, if any. michael@0: * newValue: - The new value of the attribute, if any. michael@0: * michael@0: * `characterData` type: michael@0: * newValue: - the new shortValue for the node michael@0: * [incompleteValue: true] - True if the shortValue was truncated. michael@0: * michael@0: * `childList` type is returned when the set of children for a node michael@0: * has changed. Includes extra data, which can be used by the client to michael@0: * maintain its ownership subtree. michael@0: * michael@0: * added: array of - The list of actors *previously michael@0: * seen by the client* that were added to the target node. michael@0: * removed: array of The list of actors *previously michael@0: * seen by the client* that were removed from the target node. michael@0: * michael@0: * Actors that are included in a MutationRecord's `removed` but michael@0: * not in an `added` have been removed from the client's ownership michael@0: * tree (either by being moved under a node the client has seen yet michael@0: * or by being removed from the tree entirely), and is considered michael@0: * 'orphaned'. michael@0: * michael@0: * Keep in mind that if a node that the client hasn't seen is moved michael@0: * into or out of the target node, it will not be included in the michael@0: * removedNodes and addedNodes list, so if the client is interested michael@0: * in the new set of children it needs to issue a `children` request. michael@0: */ michael@0: getMutations: method(function(options={}) { michael@0: let pending = this._pendingMutations || []; michael@0: this._pendingMutations = []; michael@0: michael@0: if (options.cleanup) { michael@0: for (let node of this._orphaned) { michael@0: // Release the orphaned node. Nodes or children that have been michael@0: // retained will be moved to this._retainedOrphans. michael@0: this.releaseNode(node); michael@0: } michael@0: this._orphaned = new Set(); michael@0: } michael@0: michael@0: return pending; michael@0: }, { michael@0: request: { michael@0: cleanup: Option(0) michael@0: }, michael@0: response: { michael@0: mutations: RetVal("array:dommutation") michael@0: } michael@0: }), michael@0: michael@0: queueMutation: function(mutation) { michael@0: if (!this.actorID) { michael@0: // We've been destroyed, don't bother queueing this mutation. michael@0: return; michael@0: } michael@0: // We only send the `new-mutations` notification once, until the client michael@0: // fetches mutations with the `getMutations` packet. michael@0: let needEvent = this._pendingMutations.length === 0; michael@0: michael@0: this._pendingMutations.push(mutation); michael@0: michael@0: if (needEvent) { michael@0: events.emit(this, "new-mutations"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Handles mutations from the DOM mutation observer API. michael@0: * michael@0: * @param array[MutationRecord] mutations michael@0: * See https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#MutationRecord michael@0: */ michael@0: onMutations: function(mutations) { michael@0: for (let change of mutations) { michael@0: let targetActor = this._refMap.get(change.target); michael@0: if (!targetActor) { michael@0: continue; michael@0: } michael@0: let targetNode = change.target; michael@0: let mutation = { michael@0: type: change.type, michael@0: target: targetActor.actorID, michael@0: } michael@0: michael@0: if (mutation.type === "attributes") { michael@0: mutation.attributeName = change.attributeName; michael@0: mutation.attributeNamespace = change.attributeNamespace || undefined; michael@0: mutation.newValue = targetNode.getAttribute(mutation.attributeName); michael@0: } else if (mutation.type === "characterData") { michael@0: if (targetNode.nodeValue.length > gValueSummaryLength) { michael@0: mutation.newValue = targetNode.nodeValue.substring(0, gValueSummaryLength); michael@0: mutation.incompleteValue = true; michael@0: } else { michael@0: mutation.newValue = targetNode.nodeValue; michael@0: } michael@0: } else if (mutation.type === "childList") { michael@0: // Get the list of removed and added actors that the client has seen michael@0: // so that it can keep its ownership tree up to date. michael@0: let removedActors = []; michael@0: let addedActors = []; michael@0: for (let removed of change.removedNodes) { michael@0: let removedActor = this._refMap.get(removed); michael@0: if (!removedActor) { michael@0: // If the client never encountered this actor we don't need to michael@0: // mention that it was removed. michael@0: continue; michael@0: } michael@0: // While removed from the tree, nodes are saved as orphaned. michael@0: this._orphaned.add(removedActor); michael@0: removedActors.push(removedActor.actorID); michael@0: } michael@0: for (let added of change.addedNodes) { michael@0: let addedActor = this._refMap.get(added); michael@0: if (!addedActor) { michael@0: // If the client never encounted this actor we don't need to tell michael@0: // it about its addition for ownership tree purposes - if the michael@0: // client wants to see the new nodes it can ask for children. michael@0: continue; michael@0: } michael@0: // The actor is reconnected to the ownership tree, unorphan michael@0: // it and let the client know so that its ownership tree is up michael@0: // to date. michael@0: this._orphaned.delete(addedActor); michael@0: addedActors.push(addedActor.actorID); michael@0: } michael@0: mutation.numChildren = change.target.childNodes.length; michael@0: mutation.removed = removedActors; michael@0: mutation.added = addedActors; michael@0: } michael@0: this.queueMutation(mutation); michael@0: } michael@0: }, michael@0: michael@0: onFrameLoad: function({ window, isTopLevel }) { michael@0: if (!this.rootDoc && isTopLevel) { michael@0: this.rootDoc = window.document; michael@0: this.rootNode = this.document(); michael@0: this.queueMutation({ michael@0: type: "newRoot", michael@0: target: this.rootNode.form() michael@0: }); michael@0: } michael@0: let frame = this.layoutHelpers.getFrameElement(window); michael@0: let frameActor = this._refMap.get(frame); michael@0: if (!frameActor) { michael@0: return; michael@0: } michael@0: michael@0: this.queueMutation({ michael@0: type: "frameLoad", michael@0: target: frameActor.actorID, michael@0: }); michael@0: michael@0: // Send a childList mutation on the frame. michael@0: this.queueMutation({ michael@0: type: "childList", michael@0: target: frameActor.actorID, michael@0: added: [], michael@0: removed: [] michael@0: }) michael@0: }, michael@0: michael@0: // Returns true if domNode is in window or a subframe. michael@0: _childOfWindow: function(window, domNode) { michael@0: let win = nodeDocument(domNode).defaultView; michael@0: while (win) { michael@0: if (win === window) { michael@0: return true; michael@0: } michael@0: win = this.layoutHelpers.getFrameElement(win); michael@0: } michael@0: return false; michael@0: }, michael@0: michael@0: onFrameUnload: function({ window }) { michael@0: // Any retained orphans that belong to this document michael@0: // or its children need to be released, and a mutation sent michael@0: // to notify of that. michael@0: let releasedOrphans = []; michael@0: michael@0: for (let retained of this._retainedOrphans) { michael@0: if (Cu.isDeadWrapper(retained.rawNode) || michael@0: this._childOfWindow(window, retained.rawNode)) { michael@0: this._retainedOrphans.delete(retained); michael@0: releasedOrphans.push(retained.actorID); michael@0: this.releaseNode(retained, { force: true }); michael@0: } michael@0: } michael@0: michael@0: if (releasedOrphans.length > 0) { michael@0: this.queueMutation({ michael@0: target: this.rootNode.actorID, michael@0: type: "unretained", michael@0: nodes: releasedOrphans michael@0: }); michael@0: } michael@0: michael@0: let doc = window.document; michael@0: let documentActor = this._refMap.get(doc); michael@0: if (!documentActor) { michael@0: return; michael@0: } michael@0: michael@0: if (this.rootDoc === doc) { michael@0: this.rootDoc = null; michael@0: this.rootNode = null; michael@0: } michael@0: michael@0: this.queueMutation({ michael@0: type: "documentUnload", michael@0: target: documentActor.actorID michael@0: }); michael@0: michael@0: let walker = documentWalker(doc, this.rootWin); michael@0: let parentNode = walker.parentNode(); michael@0: if (parentNode) { michael@0: // Send a childList mutation on the frame so that clients know michael@0: // they should reread the children list. michael@0: this.queueMutation({ michael@0: type: "childList", michael@0: target: this._refMap.get(parentNode).actorID, michael@0: added: [], michael@0: removed: [] michael@0: }); michael@0: } michael@0: michael@0: // Need to force a release of this node, because those nodes can't michael@0: // be accessed anymore. michael@0: this.releaseNode(documentActor, { force: true }); michael@0: }, michael@0: michael@0: /** michael@0: * Check if a node is attached to the DOM tree of the current page. michael@0: * @param {nsIDomNode} rawNode michael@0: * @return {Boolean} false if the node is removed from the tree or within a michael@0: * document fragment michael@0: */ michael@0: _isInDOMTree: function(rawNode) { michael@0: let walker = documentWalker(rawNode, this.rootWin); michael@0: let current = walker.currentNode; michael@0: michael@0: // Reaching the top of tree michael@0: while (walker.parentNode()) { michael@0: current = walker.currentNode; michael@0: } michael@0: michael@0: // The top of the tree is a fragment or is not rootDoc, hence rawNode isn't michael@0: // attached michael@0: if (current.nodeType === Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE || michael@0: current !== this.rootDoc) { michael@0: return false; michael@0: } michael@0: michael@0: // Otherwise the top of the tree is rootDoc, hence rawNode is in rootDoc michael@0: return true; michael@0: }, michael@0: michael@0: /** michael@0: * @see _isInDomTree michael@0: */ michael@0: isInDOMTree: method(function(node) { michael@0: return node ? this._isInDOMTree(node.rawNode) : false; michael@0: }, { michael@0: request: { node: Arg(0, "domnode") }, michael@0: response: { attached: RetVal("boolean") } michael@0: }), michael@0: michael@0: /** michael@0: * Given an ObjectActor (identified by its ID), commonly used in the debugger, michael@0: * webconsole and variablesView, return the corresponding inspector's NodeActor michael@0: */ michael@0: getNodeActorFromObjectActor: method(function(objectActorID) { michael@0: let debuggerObject = this.conn.getActor(objectActorID).obj; michael@0: let rawNode = debuggerObject.unsafeDereference(); michael@0: michael@0: if (!this._isInDOMTree(rawNode)) { michael@0: return null; michael@0: } michael@0: michael@0: // This is a special case for the document object whereby it is considered michael@0: // as document.documentElement (the node) michael@0: if (rawNode.defaultView && rawNode === rawNode.defaultView.document) { michael@0: rawNode = rawNode.documentElement; michael@0: } michael@0: michael@0: return this.attachElement(rawNode); michael@0: }, { michael@0: request: { michael@0: objectActorID: Arg(0, "string") michael@0: }, michael@0: response: { michael@0: nodeFront: RetVal("nullable:disconnectedNode") michael@0: } michael@0: }), michael@0: }); michael@0: michael@0: /** michael@0: * Client side of the DOM walker. michael@0: */ michael@0: var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, { michael@0: // Set to true if cleanup should be requested after every mutation list. michael@0: autoCleanup: true, michael@0: michael@0: /** michael@0: * This is kept for backward-compatibility reasons with older remote target. michael@0: * Targets previous to bug 916443 michael@0: */ michael@0: pick: protocol.custom(function() { michael@0: return this._pick().then(response => { michael@0: return response.node; michael@0: }); michael@0: }, {impl: "_pick"}), michael@0: michael@0: initialize: function(client, form) { michael@0: this._createRootNodePromise(); michael@0: protocol.Front.prototype.initialize.call(this, client, form); michael@0: this._orphaned = new Set(); michael@0: this._retainedOrphans = new Set(); michael@0: }, michael@0: michael@0: destroy: function() { michael@0: protocol.Front.prototype.destroy.call(this); michael@0: }, michael@0: michael@0: // Update the object given a form representation off the wire. michael@0: form: function(json) { michael@0: this.actorID = json.actor; michael@0: this.rootNode = types.getType("domnode").read(json.root, this); michael@0: this._rootNodeDeferred.resolve(this.rootNode); michael@0: }, michael@0: michael@0: /** michael@0: * Clients can use walker.rootNode to get the current root node of the michael@0: * walker, but during a reload the root node might be null. This michael@0: * method returns a promise that will resolve to the root node when it is michael@0: * set. michael@0: */ michael@0: getRootNode: function() { michael@0: return this._rootNodeDeferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Create the root node promise, triggering the "new-root" notification michael@0: * on resolution. michael@0: */ michael@0: _createRootNodePromise: function() { michael@0: this._rootNodeDeferred = promise.defer(); michael@0: this._rootNodeDeferred.promise.then(() => { michael@0: events.emit(this, "new-root"); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * When reading an actor form off the wire, we want to hook it up to its michael@0: * parent front. The protocol guarantees that the parent will be seen michael@0: * by the client in either a previous or the current request. michael@0: * So if we've already seen this parent return it, otherwise create michael@0: * a bare-bones stand-in node. The stand-in node will be updated michael@0: * with a real form by the end of the deserialization. michael@0: */ michael@0: ensureParentFront: function(id) { michael@0: let front = this.get(id); michael@0: if (front) { michael@0: return front; michael@0: } michael@0: michael@0: return types.getType("domnode").read({ actor: id }, this, "standin"); michael@0: }, michael@0: michael@0: /** michael@0: * See the documentation for WalkerActor.prototype.retainNode for michael@0: * information on retained nodes. michael@0: * michael@0: * From the client's perspective, `retainNode` can fail if the node in michael@0: * question is removed from the ownership tree before the `retainNode` michael@0: * request reaches the server. This can only happen if the client has michael@0: * asked the server to release nodes but hasn't gotten a response michael@0: * yet: Either a `releaseNode` request or a `getMutations` with `cleanup` michael@0: * set is outstanding. michael@0: * michael@0: * If either of those requests is outstanding AND releases the retained michael@0: * node, this request will fail with noSuchActor, but the ownership tree michael@0: * will stay in a consistent state. michael@0: * michael@0: * Because the protocol guarantees that requests will be processed and michael@0: * responses received in the order they were sent, we get the right michael@0: * semantics by setting our local retained flag on the node only AFTER michael@0: * a SUCCESSFUL retainNode call. michael@0: */ michael@0: retainNode: protocol.custom(function(node) { michael@0: return this._retainNode(node).then(() => { michael@0: node.retained = true; michael@0: }); michael@0: }, { michael@0: impl: "_retainNode", michael@0: }), michael@0: michael@0: unretainNode: protocol.custom(function(node) { michael@0: return this._unretainNode(node).then(() => { michael@0: node.retained = false; michael@0: if (this._retainedOrphans.has(node)) { michael@0: this._retainedOrphans.delete(node); michael@0: this._releaseFront(node); michael@0: } michael@0: }); michael@0: }, { michael@0: impl: "_unretainNode" michael@0: }), michael@0: michael@0: releaseNode: protocol.custom(function(node, options={}) { michael@0: // NodeFront.destroy will destroy children in the ownership tree too, michael@0: // mimicking what the server will do here. michael@0: let actorID = node.actorID; michael@0: this._releaseFront(node, !!options.force); michael@0: return this._releaseNode({ actorID: actorID }); michael@0: }, { michael@0: impl: "_releaseNode" michael@0: }), michael@0: michael@0: querySelector: protocol.custom(function(queryNode, selector) { michael@0: return this._querySelector(queryNode, selector).then(response => { michael@0: return response.node; michael@0: }); michael@0: }, { michael@0: impl: "_querySelector" michael@0: }), michael@0: michael@0: getNodeActorFromObjectActor: protocol.custom(function(objectActorID) { michael@0: return this._getNodeActorFromObjectActor(objectActorID).then(response => { michael@0: return response ? response.node : null; michael@0: }); michael@0: }, { michael@0: impl: "_getNodeActorFromObjectActor" michael@0: }), michael@0: michael@0: _releaseFront: function(node, force) { michael@0: if (node.retained && !force) { michael@0: node.reparent(null); michael@0: this._retainedOrphans.add(node); michael@0: return; michael@0: } michael@0: michael@0: if (node.retained) { michael@0: // Forcing a removal. michael@0: this._retainedOrphans.delete(node); michael@0: } michael@0: michael@0: // Release any children michael@0: for (let child of node.treeChildren()) { michael@0: this._releaseFront(child, force); michael@0: } michael@0: michael@0: // All children will have been removed from the node by this point. michael@0: node.reparent(null); michael@0: node.destroy(); michael@0: }, michael@0: michael@0: /** michael@0: * Get any unprocessed mutation records and process them. michael@0: */ michael@0: getMutations: protocol.custom(function(options={}) { michael@0: return this._getMutations(options).then(mutations => { michael@0: let emitMutations = []; michael@0: for (let change of mutations) { michael@0: // The target is only an actorID, get the associated front. michael@0: let targetID; michael@0: let targetFront; michael@0: michael@0: if (change.type === "newRoot") { michael@0: this.rootNode = types.getType("domnode").read(change.target, this); michael@0: this._rootNodeDeferred.resolve(this.rootNode); michael@0: targetID = this.rootNode.actorID; michael@0: targetFront = this.rootNode; michael@0: } else { michael@0: targetID = change.target; michael@0: targetFront = this.get(targetID); michael@0: } michael@0: michael@0: if (!targetFront) { michael@0: console.trace("Got a mutation for an unexpected actor: " + targetID + ", please file a bug on bugzilla.mozilla.org!"); michael@0: continue; michael@0: } michael@0: michael@0: let emittedMutation = object.merge(change, { target: targetFront }); michael@0: michael@0: if (change.type === "childList") { michael@0: // Update the ownership tree according to the mutation record. michael@0: let addedFronts = []; michael@0: let removedFronts = []; michael@0: for (let removed of change.removed) { michael@0: let removedFront = this.get(removed); michael@0: if (!removedFront) { michael@0: console.error("Got a removal of an actor we didn't know about: " + removed); michael@0: continue; michael@0: } michael@0: // Remove from the ownership tree michael@0: removedFront.reparent(null); michael@0: michael@0: // This node is orphaned unless we get it in the 'added' list michael@0: // eventually. michael@0: this._orphaned.add(removedFront); michael@0: removedFronts.push(removedFront); michael@0: } michael@0: for (let added of change.added) { michael@0: let addedFront = this.get(added); michael@0: if (!addedFront) { michael@0: console.error("Got an addition of an actor we didn't know about: " + added); michael@0: continue; michael@0: } michael@0: addedFront.reparent(targetFront) michael@0: michael@0: // The actor is reconnected to the ownership tree, unorphan michael@0: // it. michael@0: this._orphaned.delete(addedFront); michael@0: addedFronts.push(addedFront); michael@0: } michael@0: // Before passing to users, replace the added and removed actor michael@0: // ids with front in the mutation record. michael@0: emittedMutation.added = addedFronts; michael@0: emittedMutation.removed = removedFronts; michael@0: targetFront._form.numChildren = change.numChildren; michael@0: } else if (change.type === "frameLoad") { michael@0: // Nothing we need to do here, except verify that we don't have any michael@0: // document children, because we should have gotten a documentUnload michael@0: // first. michael@0: for (let child of targetFront.treeChildren()) { michael@0: if (child.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) { michael@0: console.trace("Got an unexpected frameLoad in the inspector, please file a bug on bugzilla.mozilla.org!"); michael@0: } michael@0: } michael@0: } else if (change.type === "documentUnload") { michael@0: if (targetFront === this.rootNode) { michael@0: this._createRootNodePromise(); michael@0: } michael@0: michael@0: // We try to give fronts instead of actorIDs, but these fronts need michael@0: // to be destroyed now. michael@0: emittedMutation.target = targetFront.actorID; michael@0: emittedMutation.targetParent = targetFront.parentNode(); michael@0: michael@0: // Release the document node and all of its children, even retained. michael@0: this._releaseFront(targetFront, true); michael@0: } else if (change.type === "unretained") { michael@0: // Retained orphans were force-released without the intervention of michael@0: // client (probably a navigated frame). michael@0: for (let released of change.nodes) { michael@0: let releasedFront = this.get(released); michael@0: this._retainedOrphans.delete(released); michael@0: this._releaseFront(releasedFront, true); michael@0: } michael@0: } else { michael@0: targetFront.updateMutation(change); michael@0: } michael@0: michael@0: emitMutations.push(emittedMutation); michael@0: } michael@0: michael@0: if (options.cleanup) { michael@0: for (let node of this._orphaned) { michael@0: // This will move retained nodes to this._retainedOrphans. michael@0: this._releaseFront(node); michael@0: } michael@0: this._orphaned = new Set(); michael@0: } michael@0: michael@0: events.emit(this, "mutations", emitMutations); michael@0: }); michael@0: }, { michael@0: impl: "_getMutations" michael@0: }), michael@0: michael@0: /** michael@0: * Handle the `new-mutations` notification by fetching the michael@0: * available mutation records. michael@0: */ michael@0: onMutations: protocol.preEvent("new-mutations", function() { michael@0: // Fetch and process the mutations. michael@0: this.getMutations({cleanup: this.autoCleanup}).then(null, console.error); michael@0: }), michael@0: michael@0: isLocal: function() { michael@0: return !!this.conn._transport._serverConnection; michael@0: }, michael@0: michael@0: // XXX hack during transition to remote inspector: get a proper NodeFront michael@0: // for a given local node. Only works locally. michael@0: frontForRawNode: function(rawNode) { michael@0: if (!this.isLocal()) { michael@0: console.warn("Tried to use frontForRawNode on a remote connection."); michael@0: return null; michael@0: } michael@0: let walkerActor = this.conn._transport._serverConnection.getActor(this.actorID); michael@0: if (!walkerActor) { michael@0: throw Error("Could not find client side for actor " + this.actorID); michael@0: } michael@0: let nodeActor = walkerActor._ref(rawNode); michael@0: michael@0: // Pass the node through a read/write pair to create the client side actor. michael@0: let nodeType = types.getType("domnode"); michael@0: let returnNode = nodeType.read(nodeType.write(nodeActor, walkerActor), this); michael@0: let top = returnNode; michael@0: let extras = walkerActor.parents(nodeActor); michael@0: for (let extraActor of extras) { michael@0: top = nodeType.read(nodeType.write(extraActor, walkerActor), this); michael@0: } michael@0: michael@0: if (top !== this.rootNode) { michael@0: // Imported an already-orphaned node. michael@0: this._orphaned.add(top); michael@0: walkerActor._orphaned.add(this.conn._transport._serverConnection.getActor(top.actorID)); michael@0: } michael@0: return returnNode; michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * Convenience API for building a list of attribute modifications michael@0: * for the `modifyAttributes` request. michael@0: */ michael@0: var AttributeModificationList = Class({ michael@0: initialize: function(node) { michael@0: this.node = node; michael@0: this.modifications = []; michael@0: }, michael@0: michael@0: apply: function() { michael@0: let ret = this.node.modifyAttributes(this.modifications); michael@0: return ret; michael@0: }, michael@0: michael@0: destroy: function() { michael@0: this.node = null; michael@0: this.modification = null; michael@0: }, michael@0: michael@0: setAttributeNS: function(ns, name, value) { michael@0: this.modifications.push({ michael@0: attributeNamespace: ns, michael@0: attributeName: name, michael@0: newValue: value michael@0: }); michael@0: }, michael@0: michael@0: setAttribute: function(name, value) { michael@0: this.setAttributeNS(undefined, name, value); michael@0: }, michael@0: michael@0: removeAttributeNS: function(ns, name) { michael@0: this.setAttributeNS(ns, name, undefined); michael@0: }, michael@0: michael@0: removeAttribute: function(name) { michael@0: this.setAttributeNS(undefined, name, undefined); michael@0: } michael@0: }) michael@0: michael@0: /** michael@0: * Server side of the inspector actor, which is used to create michael@0: * inspector-related actors, including the walker. michael@0: */ michael@0: var InspectorActor = protocol.ActorClass({ michael@0: typeName: "inspector", michael@0: initialize: function(conn, tabActor) { michael@0: protocol.Actor.prototype.initialize.call(this, conn); michael@0: this.tabActor = tabActor; michael@0: }, michael@0: michael@0: get window() this.tabActor.window, michael@0: michael@0: getWalker: method(function(options={}) { michael@0: if (this._walkerPromise) { michael@0: return this._walkerPromise; michael@0: } michael@0: michael@0: let deferred = promise.defer(); michael@0: this._walkerPromise = deferred.promise; michael@0: michael@0: let window = this.window; michael@0: var domReady = () => { michael@0: let tabActor = this.tabActor; michael@0: window.removeEventListener("DOMContentLoaded", domReady, true); michael@0: this.walker = WalkerActor(this.conn, tabActor, options); michael@0: events.once(this.walker, "destroyed", () => { michael@0: this._walkerPromise = null; michael@0: this._pageStylePromise = null; michael@0: }); michael@0: deferred.resolve(this.walker); michael@0: }; michael@0: michael@0: if (window.document.readyState === "loading") { michael@0: window.addEventListener("DOMContentLoaded", domReady, true); michael@0: } else { michael@0: domReady(); michael@0: } michael@0: michael@0: return this._walkerPromise; michael@0: }, { michael@0: request: {}, michael@0: response: { michael@0: walker: RetVal("domwalker") michael@0: } michael@0: }), michael@0: michael@0: getPageStyle: method(function() { michael@0: if (this._pageStylePromise) { michael@0: return this._pageStylePromise; michael@0: } michael@0: michael@0: this._pageStylePromise = this.getWalker().then(walker => { michael@0: return PageStyleActor(this); michael@0: }); michael@0: return this._pageStylePromise; michael@0: }, { michael@0: request: {}, michael@0: response: { michael@0: pageStyle: RetVal("pagestyle") michael@0: } michael@0: }), michael@0: michael@0: getHighlighter: method(function (autohide) { michael@0: if (this._highlighterPromise) { michael@0: return this._highlighterPromise; michael@0: } michael@0: michael@0: this._highlighterPromise = this.getWalker().then(walker => { michael@0: return HighlighterActor(this, autohide); michael@0: }); michael@0: return this._highlighterPromise; michael@0: }, { michael@0: request: { autohide: Arg(0, "boolean") }, michael@0: response: { michael@0: highligter: RetVal("highlighter") michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Get the node's image data if any (for canvas and img nodes). michael@0: * Returns an imageData object with the actual data being a LongStringActor michael@0: * and a size json object. michael@0: * The image data is transmitted as a base64 encoded png data-uri. michael@0: * The method rejects if the node isn't an image or if the image is missing michael@0: * michael@0: * Accepts a maxDim request parameter to resize images that are larger. This michael@0: * is important as the resizing occurs server-side so that image-data being michael@0: * transfered in the longstring back to the client will be that much smaller michael@0: */ michael@0: getImageDataFromURL: method(function(url, maxDim) { michael@0: let deferred = promise.defer(); michael@0: let img = new this.window.Image(); michael@0: michael@0: // On load, get the image data and send the response michael@0: img.onload = () => { michael@0: // imageToImageData throws an error if the image is missing michael@0: try { michael@0: let imageData = imageToImageData(img, maxDim); michael@0: deferred.resolve({ michael@0: data: LongStringActor(this.conn, imageData.data), michael@0: size: imageData.size michael@0: }); michael@0: } catch (e) { michael@0: deferred.reject(new Error("Image " + url+ " not available")); michael@0: } michael@0: } michael@0: michael@0: // If the URL doesn't point to a resource, reject michael@0: img.onerror = () => { michael@0: deferred.reject(new Error("Image " + url+ " not available")); michael@0: } michael@0: michael@0: // If the request hangs for too long, kill it to avoid queuing up other requests michael@0: // to the same actor, except if we're running tests michael@0: if (!gDevTools.testing) { michael@0: this.window.setTimeout(() => { michael@0: deferred.reject(new Error("Image " + url + " could not be retrieved in time")); michael@0: }, IMAGE_FETCHING_TIMEOUT); michael@0: } michael@0: michael@0: img.src = url; michael@0: michael@0: return deferred.promise; michael@0: }, { michael@0: request: {url: Arg(0), maxDim: Arg(1, "nullable:number")}, michael@0: response: RetVal("imageData") michael@0: }) michael@0: }); michael@0: michael@0: /** michael@0: * Client side of the inspector actor, which is used to create michael@0: * inspector-related actors, including the walker. michael@0: */ michael@0: var InspectorFront = exports.InspectorFront = protocol.FrontClass(InspectorActor, { michael@0: initialize: function(client, tabForm) { michael@0: protocol.Front.prototype.initialize.call(this, client); michael@0: this.actorID = tabForm.inspectorActor; michael@0: michael@0: // XXX: This is the first actor type in its hierarchy to use the protocol michael@0: // library, so we're going to self-own on the client side for now. michael@0: client.addActorPool(this); michael@0: this.manage(this); michael@0: }, michael@0: michael@0: destroy: function() { michael@0: delete this.walker; michael@0: protocol.Front.prototype.destroy.call(this); michael@0: }, michael@0: michael@0: getWalker: protocol.custom(function() { michael@0: return this._getWalker().then(walker => { michael@0: this.walker = walker; michael@0: return walker; michael@0: }); michael@0: }, { michael@0: impl: "_getWalker" michael@0: }), michael@0: michael@0: getPageStyle: protocol.custom(function() { michael@0: return this._getPageStyle().then(pageStyle => { michael@0: // We need a walker to understand node references from the michael@0: // node style. michael@0: if (this.walker) { michael@0: return pageStyle; michael@0: } michael@0: return this.getWalker().then(() => { michael@0: return pageStyle; michael@0: }); michael@0: }); michael@0: }, { michael@0: impl: "_getPageStyle" michael@0: }) michael@0: }); michael@0: michael@0: function documentWalker(node, rootWin, whatToShow=Ci.nsIDOMNodeFilter.SHOW_ALL) { michael@0: return new DocumentWalker(node, rootWin, whatToShow, whitespaceTextFilter, false); michael@0: } michael@0: michael@0: // Exported for test purposes. michael@0: exports._documentWalker = documentWalker; michael@0: michael@0: function nodeDocument(node) { michael@0: return node.ownerDocument || (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null); michael@0: } michael@0: michael@0: /** michael@0: * Similar to a TreeWalker, except will dig in to iframes and it doesn't michael@0: * implement the good methods like previousNode and nextNode. michael@0: * michael@0: * See TreeWalker documentation for explanations of the methods. michael@0: */ michael@0: function DocumentWalker(aNode, aRootWin, aShow, aFilter, aExpandEntityReferences) { michael@0: let doc = nodeDocument(aNode); michael@0: this.layoutHelpers = new LayoutHelpers(aRootWin); michael@0: this.walker = doc.createTreeWalker(doc, michael@0: aShow, aFilter, aExpandEntityReferences); michael@0: this.walker.currentNode = aNode; michael@0: this.filter = aFilter; michael@0: } michael@0: michael@0: DocumentWalker.prototype = { michael@0: get node() this.walker.node, michael@0: get whatToShow() this.walker.whatToShow, michael@0: get expandEntityReferences() this.walker.expandEntityReferences, michael@0: get currentNode() this.walker.currentNode, michael@0: set currentNode(aVal) this.walker.currentNode = aVal, michael@0: michael@0: /** michael@0: * Called when the new node is in a different document than michael@0: * the current node, creates a new treewalker for the document we've michael@0: * run in to. michael@0: */ michael@0: _reparentWalker: function(aNewNode) { michael@0: if (!aNewNode) { michael@0: return null; michael@0: } michael@0: let doc = nodeDocument(aNewNode); michael@0: let walker = doc.createTreeWalker(doc, michael@0: this.whatToShow, this.filter, this.expandEntityReferences); michael@0: walker.currentNode = aNewNode; michael@0: this.walker = walker; michael@0: return aNewNode; michael@0: }, michael@0: michael@0: parentNode: function() { michael@0: let currentNode = this.walker.currentNode; michael@0: let parentNode = this.walker.parentNode(); michael@0: michael@0: if (!parentNode) { michael@0: if (currentNode && currentNode.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE michael@0: && currentNode.defaultView) { michael@0: michael@0: let window = currentNode.defaultView; michael@0: let frame = this.layoutHelpers.getFrameElement(window); michael@0: if (frame) { michael@0: return this._reparentWalker(frame); michael@0: } michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: return parentNode; michael@0: }, michael@0: michael@0: firstChild: function() { michael@0: let node = this.walker.currentNode; michael@0: if (!node) michael@0: return null; michael@0: if (node.contentDocument) { michael@0: return this._reparentWalker(node.contentDocument); michael@0: } else if (node.getSVGDocument) { michael@0: return this._reparentWalker(node.getSVGDocument()); michael@0: } michael@0: return this.walker.firstChild(); michael@0: }, michael@0: michael@0: lastChild: function() { michael@0: let node = this.walker.currentNode; michael@0: if (!node) michael@0: return null; michael@0: if (node.contentDocument) { michael@0: return this._reparentWalker(node.contentDocument); michael@0: } else if (node.getSVGDocument) { michael@0: return this._reparentWalker(node.getSVGDocument()); michael@0: } michael@0: return this.walker.lastChild(); michael@0: }, michael@0: michael@0: previousSibling: function DW_previousSibling() this.walker.previousSibling(), michael@0: nextSibling: function DW_nextSibling() this.walker.nextSibling() michael@0: }; michael@0: michael@0: /** michael@0: * A tree walker filter for avoiding empty whitespace text nodes. michael@0: */ michael@0: function whitespaceTextFilter(aNode) { michael@0: if (aNode.nodeType == Ci.nsIDOMNode.TEXT_NODE && michael@0: !/[^\s]/.exec(aNode.nodeValue)) { michael@0: return Ci.nsIDOMNodeFilter.FILTER_SKIP; michael@0: } else { michael@0: return Ci.nsIDOMNodeFilter.FILTER_ACCEPT; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Given an image DOMNode, return the image data-uri. michael@0: * @param {DOMNode} node The image node michael@0: * @param {Number} maxDim Optionally pass a maximum size you want the longest michael@0: * side of the image to be resized to before getting the image data. michael@0: * @return {Object} An object containing the data-uri and size-related information michael@0: * {data: "...", size: {naturalWidth: 400, naturalHeight: 300, resized: true}} michael@0: * @throws an error if the node isn't an image or if the image is missing michael@0: */ michael@0: function imageToImageData(node, maxDim) { michael@0: let isImg = node.tagName.toLowerCase() === "img"; michael@0: let isCanvas = node.tagName.toLowerCase() === "canvas"; michael@0: michael@0: if (!isImg && !isCanvas) { michael@0: return null; michael@0: } michael@0: michael@0: // Get the image resize ratio if a maxDim was provided michael@0: let resizeRatio = 1; michael@0: let imgWidth = node.naturalWidth || node.width; michael@0: let imgHeight = node.naturalHeight || node.height; michael@0: let imgMax = Math.max(imgWidth, imgHeight); michael@0: if (maxDim && imgMax > maxDim) { michael@0: resizeRatio = maxDim / imgMax; michael@0: } michael@0: michael@0: // Extract the image data michael@0: let imageData; michael@0: // The image may already be a data-uri, in which case, save ourselves the michael@0: // trouble of converting via the canvas.drawImage.toDataURL method michael@0: if (isImg && node.src.startsWith("data:")) { michael@0: imageData = node.src; michael@0: } else { michael@0: // Create a canvas to copy the rawNode into and get the imageData from michael@0: let canvas = node.ownerDocument.createElementNS(XHTML_NS, "canvas"); michael@0: canvas.width = imgWidth * resizeRatio; michael@0: canvas.height = imgHeight * resizeRatio; michael@0: let ctx = canvas.getContext("2d"); michael@0: michael@0: // Copy the rawNode image or canvas in the new canvas and extract data michael@0: ctx.drawImage(node, 0, 0, canvas.width, canvas.height); michael@0: imageData = canvas.toDataURL("image/png"); michael@0: } michael@0: michael@0: return { michael@0: data: imageData, michael@0: size: { michael@0: naturalWidth: imgWidth, michael@0: naturalHeight: imgHeight, michael@0: resized: resizeRatio !== 1 michael@0: } michael@0: } michael@0: } michael@0: michael@0: loader.lazyGetter(this, "DOMUtils", function () { michael@0: return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); michael@0: });