/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
loader.lazyRequireGetter(
this,
"colorUtils",
"resource://devtools/shared/css/color.js",
true
);
loader.lazyRequireGetter(
this,
"AsyncUtils",
"resource://devtools/shared/async-utils.js"
);
loader.lazyRequireGetter(this, "flags", "resource://devtools/shared/flags.js");
loader.lazyRequireGetter(
this,
"DevToolsUtils",
"resource://devtools/shared/DevToolsUtils.js"
);
loader.lazyRequireGetter(
this,
"nodeFilterConstants",
"resource://devtools/shared/dom-node-filter-constants.js"
);
loader.lazyRequireGetter(
this,
"getAdjustedQuads",
"resource://devtools/shared/layout/utils.js",
true
);
loader.lazyRequireGetter(
this,
"CssLogic",
"resource://devtools/server/actors/inspector/css-logic.js",
true
);
loader.lazyRequireGetter(
this,
"getBackgroundFor",
"resource://devtools/server/actors/accessibility/audit/contrast.js",
true
);
loader.lazyRequireGetter(
this,
["loadSheetForBackgroundCalculation", "removeSheetForBackgroundCalculation"],
"resource://devtools/server/actors/utils/accessibility.js",
true
);
loader.lazyRequireGetter(
this,
"getTextProperties",
"resource://devtools/shared/accessibility.js",
true
);
const XHTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const IMAGE_FETCHING_TIMEOUT = 500;
/**
* Returns the properly cased version of the node's tag name, which can be
* used when displaying said name in the UI.
*
* @param {Node} rawNode
* Node for which we want the display name
* @return {string}
* Properly cased version of the node tag name
*/
const getNodeDisplayName = function (rawNode) {
const { implementedPseudoElement } = rawNode;
if (implementedPseudoElement) {
if (
implementedPseudoElement.startsWith("::view-transition") &&
rawNode.hasAttribute("name")
) {
return `${implementedPseudoElement}(${rawNode.getAttribute("name")})`;
}
return implementedPseudoElement;
}
if (rawNode.nodeName && !rawNode.localName) {
// The localName & prefix APIs have been moved from the Node interface to the Element
// interface. Use Node.nodeName as a fallback.
return rawNode.nodeName;
}
return (rawNode.prefix ? rawNode.prefix + ":" : "") + rawNode.localName;
};
/**
* Returns flex and grid information about a DOM node.
* In particular is it a grid flex/container and/or item?
*
* @param {DOMNode} node
* The node for which then information is required
* @return {object}
* An object like { grid: { isContainer, isItem }, flex: { isContainer, isItem } }
*/
function getNodeGridFlexType(node) {
return {
grid: getNodeGridType(node),
flex: getNodeFlexType(node),
};
}
function getNodeFlexType(node) {
return {
isContainer: node.getAsFlexContainer && !!node.getAsFlexContainer(),
isItem: !!node.parentFlexElement,
};
}
function getNodeGridType(node) {
return {
isContainer: node.hasGridFragments && node.hasGridFragments(),
isItem: !!findGridParentContainerForNode(node),
};
}
function nodeDocument(node) {
if (Cu.isDeadWrapper(node)) {
return null;
}
return (
node.ownerDocument || (node.nodeType == Node.DOCUMENT_NODE ? node : null)
);
}
function isNodeDead(node) {
return !node || !node.rawNode || Cu.isDeadWrapper(node.rawNode);
}
function isInXULDocument(el) {
const doc = nodeDocument(el);
return doc?.documentElement && doc.documentElement.namespaceURI === XUL_NS;
}
/**
* This DeepTreeWalker filter skips whitespace text nodes and anonymous content (unless
* we want them visible in the markup view, e.g. ::before, ::after, ::marker, …),
* plus anonymous content in XUL document (needed to show all elements in the browser toolbox).
*/
function standardTreeWalkerFilter(node) {
// There are a few native anonymous content that we want to show in markup
if (
node.nodeName === "_moz_generated_content_marker" ||
node.nodeName === "_moz_generated_content_before" ||
node.nodeName === "_moz_generated_content_after" ||
node.nodeName === "_moz_generated_content_backdrop"
) {
return nodeFilterConstants.FILTER_ACCEPT;
}
// Ignore empty whitespace text nodes that do not impact the layout.
if (isWhitespaceTextNode(node)) {
return nodeHasSize(node)
? nodeFilterConstants.FILTER_ACCEPT
: nodeFilterConstants.FILTER_SKIP;
}
if (node.isNativeAnonymous && !isInXULDocument(node)) {
const nodeTypeAttribute = node.getAttribute && node.getAttribute("type");
// The ::view-transition pseudo element node has a
// parent element that we don't want to display in the markup view.
// Instead, we want to directly display the ::view-transition pseudo-element.
if (nodeTypeAttribute === ":-moz-snapshot-containing-block") {
// FILTER_ACCEPT_CHILDREN means that the node won't be returned, but its children
// will be instead
return nodeFilterConstants.FILTER_ACCEPT_CHILDREN;
}
// Display all the ::view-transition* nodes
if (nodeTypeAttribute && nodeTypeAttribute.startsWith(":view-transition")) {
return nodeFilterConstants.FILTER_ACCEPT;
}
// Ignore all other native anonymous roots inside a non-XUL document.
// We need to do this to skip things like form controls, scrollbars,
// video controls, etc (see bug 1187482).
return nodeFilterConstants.FILTER_SKIP;
}
return nodeFilterConstants.FILTER_ACCEPT;
}
/**
* This DeepTreeWalker filter ignores anonymous content.
*/
function noAnonymousContentTreeWalkerFilter(node) {
// Ignore all native anonymous content inside a non-XUL document.
// We need to do this to skip things like form controls, scrollbars,
// video controls, etc (see bug 1187482).
if (!isInXULDocument(node) && node.isNativeAnonymous) {
return nodeFilterConstants.FILTER_SKIP;
}
return nodeFilterConstants.FILTER_ACCEPT;
}
/**
* This DeepTreeWalker filter is like standardTreeWalkerFilter except that
* it also includes all anonymous content (like internal form controls).
*/
function allAnonymousContentTreeWalkerFilter(node) {
// Ignore empty whitespace text nodes that do not impact the layout.
if (isWhitespaceTextNode(node)) {
return nodeHasSize(node)
? nodeFilterConstants.FILTER_ACCEPT
: nodeFilterConstants.FILTER_SKIP;
}
return nodeFilterConstants.FILTER_ACCEPT;
}
/**
* Is the given node a text node composed of whitespace only?
*
* @param {DOMNode} node
* @return {boolean}
*/
function isWhitespaceTextNode(node) {
return node.nodeType == Node.TEXT_NODE && !/[^\s]/.exec(node.nodeValue);
}
/**
* Does the given node have non-0 width and height?
*
* @param {DOMNode} node
* @return {boolean}
*/
function nodeHasSize(node) {
if (!node.getBoxQuads) {
return false;
}
const quads = node.getBoxQuads({
createFramesForSuppressedWhitespace: false,
});
return quads.some(quad => {
const bounds = quad.getBounds();
return bounds.width && bounds.height;
});
}
/**
* Returns a promise that is settled once the given HTMLImageElement has
* finished loading.
*
* @param {HTMLImageElement} image - The image element.
* @param {number} timeout - Maximum amount of time the image is allowed to load
* before the waiting is aborted. Ignored if flags.testing is set.
*
* @return {Promise} that is fulfilled once the image has loaded. If the image
* fails to load or the load takes too long, the promise is rejected.
*/
function ensureImageLoaded(image, timeout) {
const { HTMLImageElement } = image.ownerGlobal;
if (!(image instanceof HTMLImageElement)) {
return Promise.reject("image must be an HTMLImageELement");
}
if (image.complete) {
// The image has already finished loading.
return Promise.resolve();
}
// This image is still loading.
const onLoad = AsyncUtils.listenOnce(image, "load");
// Reject if loading fails.
const onError = AsyncUtils.listenOnce(image, "error").then(() => {
return Promise.reject("Image '" + image.src + "' failed to load.");
});
// Don't timeout when testing. This is never settled.
let onAbort = new Promise(() => {});
if (!flags.testing) {
// Tests are not running. Reject the promise after given timeout.
onAbort = DevToolsUtils.waitForTime(timeout).then(() => {
return Promise.reject("Image '" + image.src + "' took too long to load.");
});
}
// See which happens first.
return Promise.race([onLoad, onError, onAbort]);
}
/**
* Given an
![]()
or