summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/rules/utils/utils.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/inspector/rules/utils/utils.js357
1 files changed, 357 insertions, 0 deletions
diff --git a/devtools/client/inspector/rules/utils/utils.js b/devtools/client/inspector/rules/utils/utils.js
new file mode 100644
index 0000000000..c6a23ac59b
--- /dev/null
+++ b/devtools/client/inspector/rules/utils/utils.js
@@ -0,0 +1,357 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ VIEW_NODE_CSS_QUERY_CONTAINER,
+ VIEW_NODE_FONT_TYPE,
+ VIEW_NODE_IMAGE_URL_TYPE,
+ VIEW_NODE_INACTIVE_CSS,
+ VIEW_NODE_LOCATION_TYPE,
+ VIEW_NODE_PROPERTY_TYPE,
+ VIEW_NODE_SELECTOR_TYPE,
+ VIEW_NODE_SHAPE_POINT_TYPE,
+ VIEW_NODE_SHAPE_SWATCH,
+ VIEW_NODE_VALUE_TYPE,
+ VIEW_NODE_VARIABLE_TYPE,
+} = require("resource://devtools/client/inspector/shared/node-types.js");
+const INSET_POINT_TYPES = ["top", "right", "bottom", "left"];
+
+/**
+ * Returns the [Rule] object associated with the given node.
+ *
+ * @param {DOMNode} node
+ * The node which we want to find the [Rule] object for
+ * @param {ElementStyle} elementStyle
+ * The [ElementStyle] associated with the selected element
+ * @return {Rule|null} associated with the given node
+ */
+function getRuleFromNode(node, elementStyle) {
+ const ruleEl = node.closest(".ruleview-rule[data-rule-id]");
+ const ruleId = ruleEl ? ruleEl.dataset.ruleId : null;
+ return ruleId ? elementStyle.getRule(ruleId) : null;
+}
+
+/**
+ * Returns the [TextProperty] object associated with the given node.
+ *
+ * @param {DOMNode} node
+ * The node which we want to find the [TextProperty] object for
+ * @param {Rule|null} rule
+ * The [Rule] associated with the given node
+ * @return {TextProperty|null} associated with the given node
+ */
+function getDeclarationFromNode(node, rule) {
+ if (!rule) {
+ return null;
+ }
+
+ const declarationEl = node.closest(".ruleview-property[data-declaration-id]");
+ const declarationId = declarationEl
+ ? declarationEl.dataset.declarationId
+ : null;
+ return rule ? rule.getDeclaration(declarationId) : null;
+}
+
+/**
+ * Get the type of a given node in the Rules view.
+ *
+ * @param {DOMNode} node
+ * The node which we want information about
+ * @param {ElementStyle} elementStyle
+ * The ElementStyle to which this rule belongs
+ * @return {Object|null} containing the following props:
+ * - rule {Rule} The Rule object.
+ * - type {String} One of the VIEW_NODE_XXX_TYPE const in
+ * client/inspector/shared/node-types.
+ * - value {Object} Depends on the type of the node.
+ * - view {String} Always "rule" to indicate the rule view.
+ * Otherwise, returns null if the node isn't anything we care about.
+ */
+// eslint-disable-next-line complexity
+function getNodeInfo(node, elementStyle) {
+ if (!node) {
+ return null;
+ }
+
+ const rule = getRuleFromNode(node, elementStyle);
+ const declaration = getDeclarationFromNode(node, rule);
+ const classList = node.classList;
+
+ let type, value;
+
+ if (declaration && classList.contains("ruleview-propertyname")) {
+ type = VIEW_NODE_PROPERTY_TYPE;
+ value = {
+ property: node.textContent,
+ value: getPropertyNameAndValue(node).value,
+ enabled: declaration.enabled,
+ overridden: declaration.overridden,
+ pseudoElement: rule.pseudoElement,
+ sheetHref: rule.domRule.href,
+ textProperty: declaration,
+ };
+ } else if (declaration && classList.contains("ruleview-propertyvalue")) {
+ type = VIEW_NODE_VALUE_TYPE;
+ value = {
+ property: getPropertyNameAndValue(node).name,
+ value: node.textContent,
+ enabled: declaration.enabled,
+ overridden: declaration.overridden,
+ pseudoElement: rule.pseudoElement,
+ sheetHref: rule.domRule.href,
+ textProperty: declaration,
+ };
+ } else if (declaration && classList.contains("ruleview-font-family")) {
+ const { name: propertyName, value: propertyValue } =
+ getPropertyNameAndValue(node);
+ type = VIEW_NODE_FONT_TYPE;
+ value = {
+ property: propertyName,
+ value: propertyValue,
+ enabled: declaration.enabled,
+ overridden: declaration.overridden,
+ pseudoElement: rule.pseudoElement,
+ sheetHref: rule.domRule.href,
+ textProperty: declaration,
+ };
+ } else if (declaration && classList.contains("ruleview-shape-point")) {
+ type = VIEW_NODE_SHAPE_POINT_TYPE;
+ value = {
+ property: getPropertyNameAndValue(node).name,
+ value: node.textContent,
+ enabled: declaration.enabled,
+ overridden: declaration.overridden,
+ pseudoElement: rule.pseudoElement,
+ sheetHref: rule.domRule.href,
+ textProperty: declaration,
+ toggleActive: getShapeToggleActive(node),
+ point: getShapePoint(node),
+ };
+ } else if (declaration && classList.contains("ruleview-unused-warning")) {
+ type = VIEW_NODE_INACTIVE_CSS;
+ value = declaration.isUsed();
+ } else if (node.closest(".container-query-declaration")) {
+ type = VIEW_NODE_CSS_QUERY_CONTAINER;
+ const li = node.closest("li.container-query");
+ value = {
+ ancestorIndex: li.getAttribute("data-ancestor-index"),
+ rule,
+ };
+ } else if (declaration && classList.contains("ruleview-shapeswatch")) {
+ type = VIEW_NODE_SHAPE_SWATCH;
+ value = {
+ enabled: declaration.enabled,
+ overridden: declaration.overridden,
+ textProperty: declaration,
+ };
+ } else if (
+ declaration &&
+ (classList.contains("ruleview-variable") ||
+ classList.contains("ruleview-unmatched-variable"))
+ ) {
+ type = VIEW_NODE_VARIABLE_TYPE;
+ value = {
+ property: getPropertyNameAndValue(node).name,
+ value: node.textContent.trim(),
+ enabled: declaration.enabled,
+ overridden: declaration.overridden,
+ pseudoElement: rule.pseudoElement,
+ sheetHref: rule.domRule.href,
+ textProperty: declaration,
+ variable: node.dataset.variable,
+ };
+ } else if (
+ declaration &&
+ classList.contains("theme-link") &&
+ !classList.contains("ruleview-rule-source")
+ ) {
+ type = VIEW_NODE_IMAGE_URL_TYPE;
+ value = {
+ property: getPropertyNameAndValue(node).name,
+ value: node.parentNode.textContent,
+ url: node.href,
+ enabled: declaration.enabled,
+ overridden: declaration.overridden,
+ pseudoElement: rule.pseudoElement,
+ sheetHref: rule.domRule.href,
+ textProperty: declaration,
+ };
+ } else if (
+ classList.contains("ruleview-selector-unmatched") ||
+ classList.contains("ruleview-selector-matched") ||
+ classList.contains("ruleview-selectorcontainer") ||
+ classList.contains("ruleview-selector") ||
+ classList.contains("ruleview-selector-attribute") ||
+ classList.contains("ruleview-selector-pseudo-class") ||
+ classList.contains("ruleview-selector-pseudo-class-lock")
+ ) {
+ type = VIEW_NODE_SELECTOR_TYPE;
+ value = rule.selectorText;
+ } else if (
+ classList.contains("ruleview-rule-source") ||
+ classList.contains("ruleview-rule-source-label")
+ ) {
+ type = VIEW_NODE_LOCATION_TYPE;
+ value = rule.sheet?.href ? rule.sheet.href : rule.title;
+ } else {
+ return null;
+ }
+
+ return {
+ rule,
+ type,
+ value,
+ view: "rule",
+ };
+}
+
+/**
+ * Walk up the DOM from a given node until a parent property holder is found,
+ * and return the textContent for the name and value nodes.
+ * Stops at the first property found, so if node is inside the computed property
+ * list, the computed property will be returned
+ *
+ * @param {DOMNode} node
+ * The node to start from
+ * @return {Object} {name, value}
+ */
+function getPropertyNameAndValue(node) {
+ while (node?.classList) {
+ // Check first for ruleview-computed since it's the deepest
+ if (
+ node.classList.contains("ruleview-computed") ||
+ node.classList.contains("ruleview-property")
+ ) {
+ return {
+ name: node.querySelector(".ruleview-propertyname").textContent,
+ value: node.querySelector(".ruleview-propertyvalue").textContent,
+ };
+ }
+
+ node = node.parentNode;
+ }
+
+ return null;
+}
+
+/**
+ * Walk up the DOM from a given node until a parent property holder is found,
+ * and return an active shape toggle if one exists.
+ *
+ * @param {DOMNode} node
+ * The node to start from
+ * @returns {DOMNode} The active shape toggle node, if one exists.
+ */
+function getShapeToggleActive(node) {
+ while (node?.classList) {
+ // Check first for ruleview-computed since it's the deepest
+ if (
+ node.classList.contains("ruleview-computed") ||
+ node.classList.contains("ruleview-property")
+ ) {
+ return node.querySelector(".ruleview-shapeswatch.active");
+ }
+
+ node = node.parentNode;
+ }
+
+ return null;
+}
+
+/**
+ * Get the point associated with a shape point node.
+ *
+ * @param {DOMNode} node
+ * A shape point node
+ * @returns {String} The point associated with the given node.
+ */
+function getShapePoint(node) {
+ const classList = node.classList;
+ let point = node.dataset.point;
+ // Inset points use classes instead of data because a single span can represent
+ // multiple points.
+ const insetClasses = [];
+ classList.forEach(className => {
+ if (INSET_POINT_TYPES.includes(className)) {
+ insetClasses.push(className);
+ }
+ });
+ if (insetClasses.length) {
+ point = insetClasses.join(",");
+ }
+ return point;
+}
+
+/**
+ * Returns an array of CSS variables used in a CSS property value.
+ * If no CSS variables are used, returns an empty array.
+ *
+ * @param {String} propertyValue
+ * CSS property value (e.g. "1px solid var(--color, blue)")
+ * @return {Array}
+ * List of variable names (e.g. ["--color"])
+ *
+ */
+function getCSSVariables(propertyValue = "") {
+ const variables = [];
+ const parts = propertyValue.split(/var\(\s*--/);
+
+ if (parts.length) {
+ // Skip first part. It is the substring before the first occurence of "var(--"
+ for (let i = 1; i < parts.length; i++) {
+ // Split the part by any of the following characters expected after a variable name:
+ // comma, closing parenthesis or whitespace.
+ // Take just the first match. Anything else is either:
+ // - the fallback value, ex: ", blue" from "var(--color, blue)"
+ // - the closing parenthesis, ex: ")" from "var(--color)"
+ const variable = parts[i].split(/[,)\s+]/).shift();
+
+ if (variable) {
+ // Add back the double-dash. The initial string was split by "var(--"
+ variables.push(`--${variable}`);
+ }
+ }
+ }
+
+ return variables;
+}
+
+/**
+ * Get the CSS compatibility issue information for a given node.
+ *
+ * @param {DOMNode} node
+ * The node which we want compatibility information about
+ * @param {ElementStyle} elementStyle
+ * The ElementStyle to which this rule belongs
+ */
+async function getNodeCompatibilityInfo(node, elementStyle) {
+ const rule = getRuleFromNode(node, elementStyle);
+ const declaration = getDeclarationFromNode(node, rule);
+ const issue = await declaration.isCompatible();
+
+ return issue;
+}
+
+/**
+ * Returns true if the given CSS property value contains the given variable name.
+ *
+ * @param {String} propertyValue
+ * CSS property value (e.g. "var(--color)")
+ * @param {String} variableName
+ * CSS variable name (e.g. "--color")
+ * @return {Boolean}
+ */
+function hasCSSVariable(propertyValue, variableName) {
+ return getCSSVariables(propertyValue).includes(variableName);
+}
+
+module.exports = {
+ getCSSVariables,
+ getNodeInfo,
+ getRuleFromNode,
+ hasCSSVariable,
+ getNodeCompatibilityInfo,
+};