utils.js (11691B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { 8 VIEW_NODE_CSS_QUERY_CONTAINER, 9 VIEW_NODE_CSS_SELECTOR_WARNINGS, 10 VIEW_NODE_FONT_TYPE, 11 VIEW_NODE_IMAGE_URL_TYPE, 12 VIEW_NODE_INACTIVE_CSS, 13 VIEW_NODE_LOCATION_TYPE, 14 VIEW_NODE_PROPERTY_TYPE, 15 VIEW_NODE_SELECTOR_TYPE, 16 VIEW_NODE_SHAPE_POINT_TYPE, 17 VIEW_NODE_SHAPE_SWATCH, 18 VIEW_NODE_VALUE_TYPE, 19 VIEW_NODE_VARIABLE_TYPE, 20 } = require("resource://devtools/client/inspector/shared/node-types.js"); 21 const INSET_POINT_TYPES = ["top", "right", "bottom", "left"]; 22 23 /** 24 * Returns the [Rule] object associated with the given node. 25 * 26 * @param {DOMNode} node 27 * The node which we want to find the [Rule] object for 28 * @param {ElementStyle} elementStyle 29 * The [ElementStyle] associated with the selected element 30 * @return {Rule|null} associated with the given node 31 */ 32 function getRuleFromNode(node, elementStyle) { 33 const ruleEl = node.closest(".ruleview-rule[data-rule-id]"); 34 const ruleId = ruleEl ? ruleEl.dataset.ruleId : null; 35 return ruleId ? elementStyle.getRule(ruleId) : null; 36 } 37 38 /** 39 * Returns the [TextProperty] object associated with the given node. 40 * 41 * @param {DOMNode} node 42 * The node which we want to find the [TextProperty] object for 43 * @param {Rule|null} rule 44 * The [Rule] associated with the given node 45 * @return {TextProperty|null} associated with the given node 46 */ 47 function getDeclarationFromNode(node, rule) { 48 if (!rule) { 49 return null; 50 } 51 52 const declarationEl = node.closest(".ruleview-property[data-declaration-id]"); 53 const declarationId = declarationEl 54 ? declarationEl.dataset.declarationId 55 : null; 56 return rule ? rule.getDeclaration(declarationId) : null; 57 } 58 59 /** 60 * Get the type of a given node in the Rules view. 61 * 62 * @param {DOMNode} node 63 * The node which we want information about 64 * @param {ElementStyle} elementStyle 65 * The ElementStyle to which this rule belongs 66 * @return {object | null} containing the following props: 67 * - rule {Rule} The Rule object. 68 * - type {String} One of the VIEW_NODE_XXX_TYPE const in 69 * client/inspector/shared/node-types. 70 * - value {Object} Depends on the type of the node. 71 * - view {String} Always "rule" to indicate the rule view. 72 * Otherwise, returns null if the node isn't anything we care about. 73 */ 74 // eslint-disable-next-line complexity 75 function getNodeInfo(node, elementStyle) { 76 if (!node) { 77 return null; 78 } 79 80 const rule = getRuleFromNode(node, elementStyle); 81 const declaration = getDeclarationFromNode(node, rule); 82 const classList = node.classList; 83 84 let type, value; 85 86 if (declaration && classList.contains("ruleview-propertyname")) { 87 type = VIEW_NODE_PROPERTY_TYPE; 88 value = { 89 property: node.textContent, 90 value: getPropertyNameAndValue(node).value, 91 enabled: declaration.enabled, 92 overridden: declaration.overridden, 93 pseudoElement: rule.pseudoElement, 94 sheetHref: rule.domRule.href, 95 textProperty: declaration, 96 }; 97 } else if (declaration && classList.contains("ruleview-propertyvalue")) { 98 type = VIEW_NODE_VALUE_TYPE; 99 value = { 100 property: getPropertyNameAndValue(node).name, 101 value: node.textContent, 102 enabled: declaration.enabled, 103 overridden: declaration.overridden, 104 pseudoElement: rule.pseudoElement, 105 sheetHref: rule.domRule.href, 106 textProperty: declaration, 107 }; 108 } else if (declaration && classList.contains("ruleview-font-family")) { 109 const { name: propertyName, value: propertyValue } = 110 getPropertyNameAndValue(node); 111 type = VIEW_NODE_FONT_TYPE; 112 value = { 113 property: propertyName, 114 value: propertyValue, 115 enabled: declaration.enabled, 116 overridden: declaration.overridden, 117 pseudoElement: rule.pseudoElement, 118 sheetHref: rule.domRule.href, 119 textProperty: declaration, 120 }; 121 } else if (declaration && classList.contains("inspector-shape-point")) { 122 type = VIEW_NODE_SHAPE_POINT_TYPE; 123 value = { 124 property: getPropertyNameAndValue(node).name, 125 value: node.textContent, 126 enabled: declaration.enabled, 127 overridden: declaration.overridden, 128 pseudoElement: rule.pseudoElement, 129 sheetHref: rule.domRule.href, 130 textProperty: declaration, 131 toggleActive: getShapeToggleActive(node), 132 point: getShapePoint(node), 133 }; 134 } else if ( 135 declaration && 136 classList.contains("ruleview-inactive-css-warning") 137 ) { 138 type = VIEW_NODE_INACTIVE_CSS; 139 value = declaration.getInactiveCssData(); 140 } else if (node.closest(".container-query-declaration")) { 141 type = VIEW_NODE_CSS_QUERY_CONTAINER; 142 const containerQueryEl = node.closest(".container-query"); 143 value = { 144 ancestorIndex: containerQueryEl.getAttribute("data-ancestor-index"), 145 rule, 146 }; 147 } else if (node.classList.contains("ruleview-selector-warnings")) { 148 type = VIEW_NODE_CSS_SELECTOR_WARNINGS; 149 value = node.getAttribute("data-selector-warning-kind").split(","); 150 } else if (declaration && classList.contains("inspector-shapeswatch")) { 151 type = VIEW_NODE_SHAPE_SWATCH; 152 value = { 153 enabled: declaration.enabled, 154 overridden: declaration.overridden, 155 textProperty: declaration, 156 }; 157 } else if ( 158 declaration && 159 (classList.contains("inspector-variable") || 160 classList.contains("inspector-unmatched")) 161 ) { 162 type = VIEW_NODE_VARIABLE_TYPE; 163 value = { 164 property: getPropertyNameAndValue(node).name, 165 value: node.textContent.trim(), 166 enabled: declaration.enabled, 167 overridden: declaration.overridden, 168 pseudoElement: rule.pseudoElement, 169 sheetHref: rule.domRule.href, 170 textProperty: declaration, 171 variable: node.dataset.variable, 172 variableComputed: node.dataset.variableComputed, 173 startingStyleVariable: node.dataset.startingStyleVariable, 174 registeredProperty: { 175 initialValue: node.dataset.registeredPropertyInitialValue, 176 syntax: node.dataset.registeredPropertySyntax, 177 inherits: node.dataset.registeredPropertyInherits, 178 }, 179 outputParserOptions: declaration.editor.outputParserOptions, 180 cssProperties: declaration.editor.ruleView.cssProperties, 181 }; 182 } else if ( 183 declaration && 184 classList.contains("theme-link") && 185 !classList.contains("ruleview-rule-source") 186 ) { 187 type = VIEW_NODE_IMAGE_URL_TYPE; 188 value = { 189 property: getPropertyNameAndValue(node).name, 190 value: node.parentNode.textContent, 191 url: node.href, 192 enabled: declaration.enabled, 193 overridden: declaration.overridden, 194 pseudoElement: rule.pseudoElement, 195 sheetHref: rule.domRule.href, 196 textProperty: declaration, 197 }; 198 } else if ( 199 classList.contains("ruleview-selectors-container") || 200 classList.contains("ruleview-selector") || 201 classList.contains("ruleview-selector-element") || 202 classList.contains("ruleview-selector-attribute") || 203 classList.contains("ruleview-selector-pseudo-class") || 204 classList.contains("ruleview-selector-pseudo-class-lock") 205 ) { 206 type = VIEW_NODE_SELECTOR_TYPE; 207 value = rule.selectorText; 208 } else if ( 209 classList.contains("ruleview-rule-source") || 210 classList.contains("ruleview-rule-source-label") 211 ) { 212 type = VIEW_NODE_LOCATION_TYPE; 213 const sourceLabelEl = classList.contains("ruleview-rule-source-label") 214 ? node 215 : node.querySelector(".ruleview-rule-source-label"); 216 value = 217 sourceLabelEl.getAttribute("href") || rule.sheet?.href || rule.title; 218 } else { 219 return null; 220 } 221 222 return { 223 rule, 224 type, 225 value, 226 view: "rule", 227 }; 228 } 229 230 /** 231 * Walk up the DOM from a given node until a parent property holder is found, 232 * and return the textContent for the name and value nodes. 233 * Stops at the first property found, so if node is inside the computed property 234 * list, the computed property will be returned 235 * 236 * @param {DOMNode} node 237 * The node to start from 238 * @return {object} {name, value} 239 */ 240 function getPropertyNameAndValue(node) { 241 while (node?.classList) { 242 // Check first for ruleview-computed since it's the deepest 243 if ( 244 node.classList.contains("ruleview-computed") || 245 node.classList.contains("ruleview-property") 246 ) { 247 return { 248 name: node.querySelector(".ruleview-propertyname").textContent, 249 value: node.querySelector(".ruleview-propertyvalue").textContent, 250 }; 251 } 252 253 node = node.parentNode; 254 } 255 256 return null; 257 } 258 259 /** 260 * Walk up the DOM from a given node until a parent property holder is found, 261 * and return an active shape toggle if one exists. 262 * 263 * @param {DOMNode} node 264 * The node to start from 265 * @returns {DOMNode} The active shape toggle node, if one exists. 266 */ 267 function getShapeToggleActive(node) { 268 while (node?.classList) { 269 // Check first for ruleview-computed since it's the deepest 270 if ( 271 node.classList.contains("ruleview-computed") || 272 node.classList.contains("ruleview-property") 273 ) { 274 return node.querySelector(`.inspector-shapeswatch[aria-pressed="true"]`); 275 } 276 277 node = node.parentNode; 278 } 279 280 return null; 281 } 282 283 /** 284 * Get the point associated with a shape point node. 285 * 286 * @param {DOMNode} node 287 * A shape point node 288 * @returns {string} The point associated with the given node. 289 */ 290 function getShapePoint(node) { 291 const classList = node.classList; 292 let point = node.dataset.point; 293 // Inset points use classes instead of data because a single span can represent 294 // multiple points. 295 const insetClasses = []; 296 classList.forEach(className => { 297 if (INSET_POINT_TYPES.includes(className)) { 298 insetClasses.push(className); 299 } 300 }); 301 if (insetClasses.length) { 302 point = insetClasses.join(","); 303 } 304 return point; 305 } 306 307 /** 308 * Returns an array of CSS variables used in a CSS property value. 309 * If no CSS variables are used, returns an empty array. 310 * 311 * @param {string} propertyValue 312 * CSS property value (e.g. "1px solid var(--color, blue)") 313 * @return {Array} 314 * List of variable names (e.g. ["--color"]) 315 */ 316 function getCSSVariables(propertyValue = "") { 317 const variables = []; 318 const parts = propertyValue.split(/var\(\s*--/); 319 320 if (parts.length) { 321 // Skip first part. It is the substring before the first occurence of "var(--" 322 for (let i = 1; i < parts.length; i++) { 323 // Split the part by any of the following characters expected after a variable name: 324 // comma, closing parenthesis or whitespace. 325 // Take just the first match. Anything else is either: 326 // - the fallback value, ex: ", blue" from "var(--color, blue)" 327 // - the closing parenthesis, ex: ")" from "var(--color)" 328 const variable = parts[i].split(/[,)\s+]/).shift(); 329 330 if (variable) { 331 // Add back the double-dash. The initial string was split by "var(--" 332 variables.push(`--${variable}`); 333 } 334 } 335 } 336 337 return variables; 338 } 339 340 /** 341 * Get the CSS compatibility issue information for a given node. 342 * 343 * @param {DOMNode} node 344 * The node which we want compatibility information about 345 * @param {ElementStyle} elementStyle 346 * The ElementStyle to which this rule belongs 347 */ 348 async function getNodeCompatibilityInfo(node, elementStyle) { 349 const rule = getRuleFromNode(node, elementStyle); 350 const declaration = getDeclarationFromNode(node, rule); 351 const issue = await declaration.isCompatible(); 352 353 return issue; 354 } 355 356 module.exports = { 357 getCSSVariables, 358 getNodeInfo, 359 getRuleFromNode, 360 getNodeCompatibilityInfo, 361 };