mirror of
https://github.com/mozilla-firefox/firefox.git
synced 2026-06-18 12:14:01 -05:00
We need to register the devtool-html-content listener before the iframe's WindowGlobal is created. The HTML parsing starts before we are notified about its creation. Also it would be handy to pass data about the BrowsingContext/WindowGlobal from the HTML Parser in order to better organize the text content by context. Differential Revision: https://phabricator.services.mozilla.com/D258647
178 lines
6.3 KiB
JavaScript
178 lines
6.3 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
/**
|
|
* Helper class spawn independently from target to watcher for HTML source content
|
|
* being emitted by the C++ HTML Parser.
|
|
*
|
|
* As HTML sources are parsed before the HTML document's WindowGlobal is created,
|
|
* they will be parsed before the target gets created.
|
|
* This class is instantiated as soon as we start watching for WindowGlobal's
|
|
* and will hold HTML sources and deliver them on-demand to SourcesManager.
|
|
*
|
|
* This cache clears itself automatically when the WindowGlobals are destroyed.
|
|
*/
|
|
class HtmlCache {
|
|
// Map<string ->
|
|
// Map<string -> Object{
|
|
// content: string
|
|
// complete: boolean,
|
|
// parserID: string,
|
|
// }>
|
|
// >
|
|
// Nested Maps of source objects keyed by source url, stored into
|
|
// this map keyed by Browsing Context ID.
|
|
#sourcesByBrowsingContext = new Map();
|
|
|
|
// Map<string: number>
|
|
// Number of active listeners (WindowGlobal target watchers) for all currently observed browser element IDs.
|
|
#noOfActiveTargetWatchers = new Map();
|
|
|
|
// Map<number -> number>
|
|
// Map the browsing context ID for each currently active Window global InnerWindowID
|
|
//
|
|
// We have to keep track of the relationship between innerWindowID and browsingContextID
|
|
// as inner-window-destroyed only emits the innerWindowID number, from which
|
|
// we can't derivate the browsingContextID required to clear `#sourcesByBrowsingContext` Map.
|
|
#browsingContextIDByInnerWindowId = new Map();
|
|
|
|
constructor() {
|
|
Services.obs.addObserver(this, "devtools-html-content");
|
|
Services.obs.addObserver(this, "document-element-inserted");
|
|
Services.obs.addObserver(this, "inner-window-destroyed");
|
|
}
|
|
|
|
destroy() {
|
|
Services.obs.removeObserver(this, "devtools-html-content");
|
|
Services.obs.removeObserver(this, "document-element-inserted");
|
|
Services.obs.removeObserver(this, "inner-window-destroyed");
|
|
}
|
|
|
|
watch(browserId) {
|
|
const noOfTargetWatchers =
|
|
this.#noOfActiveTargetWatchers.get(browserId) || 0;
|
|
this.#noOfActiveTargetWatchers.set(browserId, noOfTargetWatchers + 1);
|
|
}
|
|
|
|
unwatch(browserId) {
|
|
const noOfTargetWatchers = this.#noOfActiveTargetWatchers.get(browserId);
|
|
if (noOfTargetWatchers > 1) {
|
|
this.#noOfActiveTargetWatchers.set(browserId, noOfTargetWatchers - 1);
|
|
} else {
|
|
this.#noOfActiveTargetWatchers.delete(browserId);
|
|
}
|
|
}
|
|
|
|
get(browsingContextID, url, partial) {
|
|
const sourcesByUrl = this.#sourcesByBrowsingContext.get(browsingContextID);
|
|
if (sourcesByUrl) {
|
|
const source = sourcesByUrl.get(url);
|
|
if (source) {
|
|
if (!partial && !source.complete) {
|
|
return source.onComplete.then(() => {
|
|
return {
|
|
content: source.content,
|
|
contentType: "text/html",
|
|
};
|
|
});
|
|
}
|
|
return {
|
|
content: source.content,
|
|
contentType: "text/html",
|
|
};
|
|
}
|
|
}
|
|
if (partial) {
|
|
return {
|
|
content: "",
|
|
contentType: "",
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Listener for new HTML content.
|
|
*/
|
|
observe(subject, topic, data) {
|
|
if (topic === "inner-window-destroyed") {
|
|
const innerWindowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
|
|
const browsingContextID =
|
|
this.#browsingContextIDByInnerWindowId.get(innerWindowId);
|
|
if (browsingContextID) {
|
|
this.#sourcesByBrowsingContext.delete(browsingContextID);
|
|
this.#browsingContextIDByInnerWindowId.delete(innerWindowId);
|
|
}
|
|
} else if (topic === "document-element-inserted") {
|
|
const window = subject.defaultView;
|
|
// Ignore any non-HTML document
|
|
if (!window) {
|
|
return;
|
|
}
|
|
const innerWindowId = window.windowGlobalChild.innerWindowId;
|
|
const browsingContextID = window.browsingContext.id;
|
|
this.#browsingContextIDByInnerWindowId.set(
|
|
innerWindowId,
|
|
browsingContextID
|
|
);
|
|
} else if (topic === "devtools-html-content") {
|
|
const {
|
|
parserID,
|
|
browserId,
|
|
browsingContextID,
|
|
uri,
|
|
contents,
|
|
complete,
|
|
} = JSON.parse(data);
|
|
|
|
// Only save data if the related browser element is being observed
|
|
if (!this.#noOfActiveTargetWatchers.has(browserId)) {
|
|
return;
|
|
}
|
|
|
|
let sourcesByUrl = this.#sourcesByBrowsingContext.get(browsingContextID);
|
|
if (!sourcesByUrl) {
|
|
sourcesByUrl = new Map();
|
|
this.#sourcesByBrowsingContext.set(browsingContextID, sourcesByUrl);
|
|
}
|
|
const source = sourcesByUrl.get(uri);
|
|
if (source) {
|
|
// We received many devtools-html-content events, if we already received one,
|
|
// aggregate the data with the one we already received.
|
|
if (source.parserID == parserID) {
|
|
source.content += contents;
|
|
source.complete = complete;
|
|
|
|
// After the HTML has finished loading, resolve any promises
|
|
// waiting for the complete file contents. Waits will only
|
|
// occur when the URL was ever partially loaded.
|
|
if (complete) {
|
|
source.resolveComplete();
|
|
}
|
|
}
|
|
} else if (contents) {
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
// Ensure that `contents` is non-empty. We may miss all the devtools-html-content events except the last
|
|
// one which has an empty `contents` and `complete` set to true.
|
|
// This reproduces when opening a same-process iframe. In this particular scenario, we instantiate the target and thread actor
|
|
// on `DOMDocElementInserted` and the HTML document is already parsed, but we still receive this one very last notification.
|
|
sourcesByUrl.set(uri, {
|
|
// String: HTML source text content
|
|
content: contents,
|
|
// Boolean: is the source complete
|
|
complete,
|
|
// String: uuid generated by the html parser to uniquely identify a document
|
|
parserID,
|
|
// Promise: resolved when the source is complete
|
|
onComplete: promise,
|
|
// Function: to be called when the source is complete
|
|
resolveComplete: resolve,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export const HTMLSourcesCache = new HtmlCache();
|