style-rule.js (54079B)
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 { Actor } = require("resource://devtools/shared/protocol.js"); 8 const { 9 styleRuleSpec, 10 } = require("resource://devtools/shared/specs/style-rule.js"); 11 12 const { 13 InspectorCSSParserWrapper, 14 } = require("resource://devtools/shared/css/lexer.js"); 15 const { 16 getRuleText, 17 getTextAtLineColumn, 18 } = require("resource://devtools/server/actors/utils/style-utils.js"); 19 20 const { 21 style: { ELEMENT_STYLE, PRES_HINTS }, 22 } = require("resource://devtools/shared/constants.js"); 23 24 loader.lazyRequireGetter( 25 this, 26 "CssLogic", 27 "resource://devtools/server/actors/inspector/css-logic.js", 28 true 29 ); 30 loader.lazyRequireGetter( 31 this, 32 "getNodeDisplayName", 33 "resource://devtools/server/actors/inspector/utils.js", 34 true 35 ); 36 loader.lazyRequireGetter( 37 this, 38 "SharedCssLogic", 39 "resource://devtools/shared/inspector/css-logic.js" 40 ); 41 loader.lazyRequireGetter( 42 this, 43 "isCssPropertyKnown", 44 "resource://devtools/server/actors/css-properties.js", 45 true 46 ); 47 loader.lazyRequireGetter( 48 this, 49 "getInactiveCssDataForProperty", 50 "resource://devtools/server/actors/utils/inactive-property-helper.js", 51 true 52 ); 53 loader.lazyRequireGetter( 54 this, 55 "parseNamedDeclarations", 56 "resource://devtools/shared/css/parsing-utils.js", 57 true 58 ); 59 loader.lazyRequireGetter( 60 this, 61 ["UPDATE_PRESERVING_RULES", "UPDATE_GENERAL"], 62 "resource://devtools/server/actors/utils/stylesheets-manager.js", 63 true 64 ); 65 loader.lazyRequireGetter( 66 this, 67 "DocumentWalker", 68 "devtools/server/actors/inspector/document-walker", 69 true 70 ); 71 72 const XHTML_NS = "http://www.w3.org/1999/xhtml"; 73 74 /** 75 * An actor that represents a CSS style object on the protocol. 76 * 77 * We slightly flatten the CSSOM for this actor, it represents 78 * both the CSSRule and CSSStyle objects in one actor. For nodes 79 * (which have a CSSStyle but no CSSRule) we create a StyleRuleActor 80 * with a special rule type (100). 81 */ 82 class StyleRuleActor extends Actor { 83 /** 84 * 85 * @param {object} options 86 * @param {PageStyleActor} options.pageStyle 87 * @param {CSSStyleRule|Element} options.item 88 * @param {boolean} options.userAdded: Optional boolean to distinguish rules added by the user. 89 * @param {string} options.pseudoElement An optional pseudo-element type in cases when 90 * the CSS rule applies to a pseudo-element. 91 */ 92 constructor({ pageStyle, item, userAdded = false, pseudoElement = null }) { 93 super(pageStyle.conn, styleRuleSpec); 94 this.pageStyle = pageStyle; 95 this.rawStyle = item.style; 96 this._userAdded = userAdded; 97 this._pseudoElements = new Set(); 98 this._pseudoElement = pseudoElement; 99 if (pseudoElement) { 100 this._pseudoElements.add(pseudoElement); 101 } 102 this._parentSheet = null; 103 // Parsed CSS declarations from this.form().declarations used to check CSS property 104 // names and values before tracking changes. Using cached values instead of accessing 105 // this.form().declarations on demand because that would cause needless re-parsing. 106 this._declarations = []; 107 108 this._pendingDeclarationChanges = []; 109 this._failedToGetRuleText = false; 110 111 if (CSSRule.isInstance(item)) { 112 this.type = item.type; 113 this.ruleClassName = ChromeUtils.getClassName(item); 114 115 this.rawRule = item; 116 this._computeRuleIndex(); 117 if (this.#isRuleSupported() && this.rawRule.parentStyleSheet) { 118 this.line = InspectorUtils.getRelativeRuleLine(this.rawRule); 119 this.column = InspectorUtils.getRuleColumn(this.rawRule); 120 this._parentSheet = this.rawRule.parentStyleSheet; 121 } 122 } else if (item.declarationOrigin === "pres-hints") { 123 this.type = PRES_HINTS; 124 this.ruleClassName = PRES_HINTS; 125 this.rawNode = item; 126 this.rawRule = { 127 style: item.style, 128 toString() { 129 return "[element attribute styles " + this.style + "]"; 130 }, 131 }; 132 } else { 133 // Fake a rule 134 this.type = ELEMENT_STYLE; 135 this.ruleClassName = ELEMENT_STYLE; 136 this.rawNode = item; 137 this.rawRule = { 138 style: item.style, 139 toString() { 140 return "[element rule " + this.style + "]"; 141 }, 142 }; 143 } 144 } 145 146 destroy() { 147 if (!this.rawStyle) { 148 return; 149 } 150 super.destroy(); 151 this.rawStyle = null; 152 this.pageStyle = null; 153 this.rawNode = null; 154 this.rawRule = null; 155 this._declarations = null; 156 if (this._pseudoElements) { 157 this._pseudoElements.clear(); 158 this._pseudoElements = null; 159 } 160 } 161 162 // Objects returned by this actor are owned by the PageStyleActor 163 // to which this rule belongs. 164 get marshallPool() { 165 return this.pageStyle; 166 } 167 168 // True if this rule supports as-authored styles, meaning that the 169 // rule text can be rewritten using setRuleText. 170 get canSetRuleText() { 171 if (this.type === ELEMENT_STYLE) { 172 // Element styles are always editable. 173 return true; 174 } 175 if (!this._parentSheet) { 176 return false; 177 } 178 if (InspectorUtils.hasRulesModifiedByCSSOM(this._parentSheet)) { 179 // If a rule has been modified via CSSOM, then we should fall back to 180 // non-authored editing. 181 // https://bugzilla.mozilla.org/show_bug.cgi?id=1224121 182 return false; 183 } 184 return true; 185 } 186 187 /** 188 * Return an array with StyleRuleActor instances for each of this rule's ancestor rules 189 * (@media, @supports, @keyframes, etc) obtained by recursively reading rule.parentRule. 190 * If the rule has no ancestors, return an empty array. 191 * 192 * @return {Array} 193 */ 194 get ancestorRules() { 195 const ancestors = []; 196 let rule = this.rawRule; 197 198 while (rule.parentRule) { 199 ancestors.unshift(this.pageStyle.styleRef(rule.parentRule)); 200 rule = rule.parentRule; 201 } 202 203 return ancestors; 204 } 205 206 /** 207 * Return an object with information about this rule used for tracking changes. 208 * It will be decorated with information about a CSS change before being tracked. 209 * 210 * It contains: 211 * - the rule selector (or generated selectror for inline styles) 212 * - the rule's host stylesheet (or element for inline styles) 213 * - the rule's ancestor rules (@media, @supports, @keyframes), if any 214 * - the rule's position within its ancestor tree, if any 215 * 216 * @return {object} 217 */ 218 get metadata() { 219 const data = {}; 220 data.id = this.actorID; 221 // Collect information about the rule's ancestors (@media, @supports, @keyframes, parent rules). 222 // Used to show context for this change in the UI and to match the rule for undo/redo. 223 data.ancestors = this.ancestorRules.map(rule => { 224 const ancestorData = { 225 id: rule.actorID, 226 // Array with the indexes of this rule and its ancestors within the CSS rule tree. 227 ruleIndex: rule._ruleIndex, 228 }; 229 230 // Rule type as human-readable string (ex: "@media", "@supports", "@keyframes") 231 const typeName = SharedCssLogic.getCSSAtRuleTypeName(rule.rawRule); 232 if (typeName) { 233 ancestorData.typeName = typeName; 234 } 235 236 // Conditions of @container, @media and @supports rules (ex: "min-width: 1em") 237 if (rule.rawRule.conditionText !== undefined) { 238 ancestorData.conditionText = rule.rawRule.conditionText; 239 } 240 241 // Name of @keyframes rule; referenced by the animation-name CSS property. 242 if (rule.rawRule.name !== undefined) { 243 ancestorData.name = rule.rawRule.name; 244 } 245 246 // Selector of individual @keyframe rule within a @keyframes rule (ex: 0%, 100%). 247 if (rule.rawRule.keyText !== undefined) { 248 ancestorData.keyText = rule.rawRule.keyText; 249 } 250 251 // Selector of the rule; might be useful in case for nested rules 252 if (rule.rawRule.selectorText !== undefined) { 253 ancestorData.selectorText = rule.rawRule.selectorText; 254 } 255 256 return ancestorData; 257 }); 258 259 // For changes in element style attributes, generate a unique selector. 260 if (this.type === ELEMENT_STYLE && this.rawNode) { 261 // findCssSelector() fails on XUL documents. Catch and silently ignore that error. 262 try { 263 data.selector = SharedCssLogic.findCssSelector(this.rawNode); 264 } catch (err) {} 265 266 data.source = { 267 type: "element", 268 // Used to differentiate between elements which match the same generated selector 269 // but live in different documents (ex: host document and iframe). 270 href: this.rawNode.baseURI, 271 // Element style attributes don't have a rule index; use the generated selector. 272 index: data.selector, 273 // Whether the element lives in a different frame than the host document. 274 isFramed: this.rawNode.ownerGlobal !== this.pageStyle.ownerWindow, 275 }; 276 277 const nodeActor = this.pageStyle.walker.getNode(this.rawNode); 278 if (nodeActor) { 279 data.source.id = nodeActor.actorID; 280 } 281 282 data.ruleIndex = 0; 283 } else { 284 data.selector = 285 this.ruleClassName === "CSSKeyframeRule" 286 ? this.rawRule.keyText 287 : this.rawRule.selectorText; 288 // Used to differentiate between changes to rules with identical selectors. 289 data.ruleIndex = this._ruleIndex; 290 291 const sheet = this._parentSheet; 292 const inspectorActor = this.pageStyle.inspector; 293 const resourceId = 294 this.pageStyle.styleSheetsManager.getStyleSheetResourceId(sheet); 295 data.source = { 296 // Inline stylesheets have a null href; Use window URL instead. 297 type: sheet.href ? "stylesheet" : "inline", 298 href: sheet.href || inspectorActor.window.location.toString(), 299 id: resourceId, 300 // Whether the stylesheet lives in a different frame than the host document. 301 isFramed: inspectorActor.window !== inspectorActor.window.top, 302 }; 303 } 304 305 return data; 306 } 307 308 /** 309 * Returns true if the pseudo element anonymous node (e.g. ::before, ::marker, …) is selected. 310 * Returns false if a non pseudo element node is selected and we're looking into its pseudo 311 * elements rules (i.e. this is for the "Pseudo-elements" section in the Rules view") 312 */ 313 get isPseudoElementAnonymousNodeSelected() { 314 if (!this._pseudoElement) { 315 return false; 316 } 317 318 // `this._pseudoElement` is the returned value by getNodeDisplayName, i.e that does 319 // differ from this.pageStyle.selectedElement.implementedPseudoElement (e.g. for 320 // view transition element, it will be `::view-transition-group(root)`, while 321 // implementedPseudoElement will be `::view-transition-group`). 322 return ( 323 this._pseudoElement === getNodeDisplayName(this.pageStyle.selectedElement) 324 ); 325 } 326 327 /** 328 * StyleRuleActor is spawned once per CSS Rule, but will be refreshed based on the 329 * currently selected DOM Element, which is updated when PageStyleActor.getApplied 330 * is called. 331 */ 332 get currentlySelectedElement() { 333 let { selectedElement } = this.pageStyle; 334 // If we're not handling a pseudo element, or if the pseudo element node 335 // (e.g. ::before, ::marker, …) is the one selected in the markup view, we can 336 // directly return selected element. 337 if (!this._pseudoElement || this.isPseudoElementAnonymousNodeSelected) { 338 return selectedElement; 339 } 340 341 // Otherwise we are selecting the pseudo element "parent" (binding), and we need to 342 // walk down the tree from `selectedElement` to find the pseudo element. 343 344 // FIXME: ::view-transition pseudo elements don't have a _moz_generated_content_ prefixed 345 // nodename, but have specific type and name attribute. 346 // At the moment this isn't causing any issues because we don't display the view 347 // transition rules in the pseudo element section, but this should be fixed in Bug 1998345. 348 const pseudo = this._pseudoElement.replaceAll(":", ""); 349 const nodeName = `_moz_generated_content_${pseudo}`; 350 351 if (selectedElement.nodeName !== nodeName) { 352 const walker = new DocumentWalker( 353 selectedElement, 354 selectedElement.ownerGlobal 355 ); 356 357 for (let next = walker.firstChild(); next; next = walker.nextSibling()) { 358 if (next.nodeName === nodeName) { 359 selectedElement = next; 360 break; 361 } 362 } 363 } 364 365 return selectedElement; 366 } 367 368 get currentlySelectedElementComputedStyle() { 369 if (!this._pseudoElement) { 370 return this.pageStyle.cssLogic.computedStyle; 371 } 372 373 const { selectedElement } = this.pageStyle; 374 375 return selectedElement.ownerGlobal.getComputedStyle( 376 selectedElement, 377 // If we are selecting the pseudo element parent, we need to pass the pseudo element 378 // to getComputedStyle to actually get the computed style of the pseudo element. 379 !this.isPseudoElementAnonymousNodeSelected ? this._pseudoElement : null 380 ); 381 } 382 383 get pseudoElements() { 384 return this._pseudoElements; 385 } 386 387 addPseudo(pseudoElement) { 388 this._pseudoElements.add(pseudoElement); 389 } 390 391 getDocument(sheet) { 392 if (!sheet.associatedDocument) { 393 throw new Error( 394 "Failed trying to get the document of an invalid stylesheet" 395 ); 396 } 397 return sheet.associatedDocument; 398 } 399 400 toString() { 401 return "[StyleRuleActor for " + this.rawRule + "]"; 402 } 403 404 // eslint-disable-next-line complexity 405 form() { 406 const form = { 407 actor: this.actorID, 408 type: this.type, 409 className: this.ruleClassName, 410 line: this.line || undefined, 411 column: this.column, 412 traits: { 413 // Indicates whether StyleRuleActor implements and can use the setRuleText method. 414 // It cannot use it if the stylesheet was programmatically mutated via the CSSOM. 415 canSetRuleText: this.canSetRuleText, 416 }, 417 }; 418 419 // This rule was manually added by the user and may be automatically focused by the frontend. 420 if (this._userAdded) { 421 form.userAdded = true; 422 } 423 424 form.ancestorData = this._getAncestorDataForForm(); 425 426 if (this._parentSheet) { 427 form.parentStyleSheet = 428 this.pageStyle.styleSheetsManager.getStyleSheetResourceId( 429 this._parentSheet 430 ); 431 } 432 433 // One tricky thing here is that other methods in this actor must 434 // ensure that authoredText has been set before |form| is called. 435 // This has to be treated specially, for now, because we cannot 436 // synchronously compute the authored text, but |form| also cannot 437 // return a promise. See bug 1205868. 438 form.authoredText = this.authoredText; 439 form.cssText = this._getCssText(); 440 441 switch (this.ruleClassName) { 442 case "CSSNestedDeclarations": 443 form.isNestedDeclarations = true; 444 form.selectors = []; 445 form.selectorsSpecificity = []; 446 break; 447 case "CSSStyleRule": { 448 form.selectors = []; 449 form.selectorsSpecificity = []; 450 451 for (let i = 0, len = this.rawRule.selectorCount; i < len; i++) { 452 form.selectors.push(this.rawRule.selectorTextAt(i)); 453 form.selectorsSpecificity.push( 454 this.rawRule.selectorSpecificityAt( 455 i, 456 /* desugared, so we get the actual specificity */ true 457 ) 458 ); 459 } 460 461 // Only add the property when there are elements in the array to save up on serialization. 462 const selectorWarnings = this.rawRule.getSelectorWarnings(); 463 if (selectorWarnings.length) { 464 form.selectorWarnings = selectorWarnings; 465 } 466 break; 467 } 468 case ELEMENT_STYLE: { 469 // Elements don't have a parent stylesheet, and therefore 470 // don't have an associated URI. Provide a URI for 471 // those. 472 const doc = this.rawNode.ownerDocument; 473 form.href = doc.location ? doc.location.href : ""; 474 form.authoredText = this.rawNode.getAttribute("style"); 475 break; 476 } 477 case PRES_HINTS: 478 form.href = ""; 479 break; 480 case "CSSCharsetRule": 481 form.encoding = this.rawRule.encoding; 482 break; 483 case "CSSImportRule": 484 form.href = this.rawRule.href; 485 break; 486 case "CSSKeyframesRule": 487 case "CSSPositionTryRule": 488 form.name = this.rawRule.name; 489 break; 490 case "CSSKeyframeRule": 491 form.keyText = this.rawRule.keyText || ""; 492 break; 493 } 494 495 // Parse the text into a list of declarations so the client doesn't have to 496 // and so that we can safely determine if a declaration is valid rather than 497 // have the client guess it. 498 if (form.authoredText || form.cssText) { 499 const declarations = this.parseRuleDeclarations({ 500 parseComments: true, 501 }); 502 const el = this.currentlySelectedElement; 503 const style = this.currentlySelectedElementComputedStyle; 504 505 // Whether the stylesheet is a user-agent stylesheet. This affects the 506 // validity of some properties and property values. 507 const userAgent = 508 this._parentSheet && 509 SharedCssLogic.isAgentStylesheet(this._parentSheet); 510 // Whether the stylesheet is a chrome stylesheet. Ditto. 511 // 512 // Note that chrome rules are also enabled in user sheets, see 513 // ParserContext::chrome_rules_enabled(). 514 // 515 // https://searchfox.org/mozilla-central/rev/919607a3610222099fbfb0113c98b77888ebcbfb/servo/components/style/parser.rs#164 516 const chrome = (() => { 517 if (!this._parentSheet) { 518 return false; 519 } 520 if (SharedCssLogic.isUserStylesheet(this._parentSheet)) { 521 return true; 522 } 523 if (this._parentSheet.href) { 524 return this._parentSheet.href.startsWith("chrome:"); 525 } 526 return el && el.ownerDocument.documentURI.startsWith("chrome:"); 527 })(); 528 // Whether the document is in quirks mode. This affects whether stuff 529 // like `width: 10` is valid. 530 const quirks = 531 !userAgent && el && el.ownerDocument.compatMode == "BackCompat"; 532 const supportsOptions = { userAgent, chrome, quirks }; 533 534 const targetDocument = 535 this.pageStyle.inspector.targetActor.window.document; 536 let registeredProperties; 537 538 form.declarations = declarations.map(decl => { 539 // InspectorUtils.supports only supports the 1-arg version, but that's 540 // what we want to do anyways so that we also accept !important in the 541 // value. 542 decl.isValid = 543 // Always consider pres hints styles declarations valid. We need this because 544 // in some cases we might get quirks declarations for which we serialize the 545 // value to something meaningful for the user, but that can't be actually set. 546 // (e.g. for <table> in quirks mode, we get a `color: -moz-inherit-from-body-quirk`) 547 // In such case InspectorUtils.supports() would return false, but that would be 548 // odd to show "invalid" pres hints declaration in the UI. 549 this.ruleClassName === PRES_HINTS || 550 (InspectorUtils.supports( 551 `${decl.name}:${decl.value}`, 552 supportsOptions 553 ) && 554 // !important values are not valid in @position-try and @keyframes 555 // TODO: We might extend InspectorUtils.supports to take the actual rule 556 // so we wouldn't have to hardcode this, but this does come with some 557 // challenges (see Bug 2004379). 558 !( 559 decl.priority === "important" && 560 (this.ruleClassName === "CSSPositionTryRule" || 561 this.ruleClassName === "CSSKeyframesRule") 562 )); 563 const inactiveCssData = getInactiveCssDataForProperty( 564 el, 565 style, 566 this.rawRule, 567 decl.name 568 ); 569 if (inactiveCssData !== null) { 570 decl.inactiveCssData = inactiveCssData; 571 } 572 573 // Check property name. All valid CSS properties support "initial" as a value. 574 decl.isNameValid = 575 // InspectorUtils.supports can be costly, don't call it when the declaration 576 // is a CSS variable, it should always be valid 577 decl.isCustomProperty || 578 InspectorUtils.supports(`${decl.name}:initial`, supportsOptions); 579 580 if (decl.isCustomProperty) { 581 decl.computedValue = style.getPropertyValue(decl.name); 582 583 // If the variable is a registered property, we check if the variable is 584 // invalid at computed-value time (e.g. if the declaration value matches 585 // the `syntax` defined in the registered property) 586 if (!registeredProperties) { 587 registeredProperties = 588 InspectorUtils.getCSSRegisteredProperties(targetDocument); 589 } 590 const registeredProperty = registeredProperties.find( 591 prop => prop.name === decl.name 592 ); 593 if ( 594 registeredProperty && 595 // For now, we don't handle variable based on top of other variables. This would 596 // require to build some kind of dependency tree and check the validity for 597 // all the leaves. 598 !decl.value.includes("var(") && 599 !InspectorUtils.valueMatchesSyntax( 600 targetDocument, 601 decl.value, 602 registeredProperty.syntax 603 ) 604 ) { 605 // if the value doesn't match the syntax, it's invalid 606 decl.invalidAtComputedValueTime = true; 607 // pass the syntax down to the client so it can easily be used in a warning message 608 decl.syntax = registeredProperty.syntax; 609 } 610 611 // We only compute `inherits` for css variable declarations. 612 // For "regular" declaration, we use `CssPropertiesFront.isInherited`, 613 // which doesn't depend on the state of the document (a given property will 614 // always have the same isInherited value). 615 // CSS variables on the other hand can be registered custom properties (e.g., 616 // `@property`/`CSS.registerProperty`), with a `inherits` definition that can 617 // be true or false. 618 // As such custom properties can be registered at any time during the page 619 // lifecycle, we always recompute the `inherits` information for CSS variables. 620 decl.inherits = InspectorUtils.isInheritedProperty( 621 this.pageStyle.inspector.window.document, 622 decl.name 623 ); 624 } 625 626 return decl; 627 }); 628 629 // We have computed the new `declarations` array, before forgetting about 630 // the old declarations compute the CSS changes for pending modifications 631 // applied by the user. Comparing the old and new declarations arrays 632 // ensures we only rely on values understood by the engine and not authored 633 // values. See Bug 1590031. 634 this._pendingDeclarationChanges.forEach(change => 635 this.logDeclarationChange(change, declarations, this._declarations) 636 ); 637 this._pendingDeclarationChanges = []; 638 639 // Cache parsed declarations so we don't needlessly re-parse authoredText every time 640 // we need to check previous property names and values when tracking changes. 641 this._declarations = declarations; 642 } 643 644 return form; 645 } 646 647 /** 648 * Return the rule cssText if applicable, null otherwise 649 * 650 * @returns {string | null} 651 */ 652 _getCssText() { 653 switch (this.ruleClassName) { 654 case "CSSNestedDeclarations": 655 case "CSSPositionTryRule": 656 case "CSSStyleRule": 657 case ELEMENT_STYLE: 658 case PRES_HINTS: 659 return this.rawStyle.cssText || ""; 660 case "CSSKeyframesRule": 661 case "CSSKeyframeRule": 662 return this.rawRule.cssText; 663 } 664 return null; 665 } 666 667 /** 668 * Parse the rule declarations from its text. 669 * 670 * @param {object} options 671 * @param {boolean} options.parseComments 672 * @returns {Array} @see parseNamedDeclarations 673 */ 674 parseRuleDeclarations({ parseComments }) { 675 const authoredText = 676 this.ruleClassName === ELEMENT_STYLE 677 ? this.rawNode.getAttribute("style") 678 : this.authoredText; 679 680 // authoredText may be an empty string when deleting all properties; it's ok to use. 681 const cssText = 682 typeof authoredText === "string" ? authoredText : this._getCssText(); 683 if (!cssText) { 684 return []; 685 } 686 687 return parseNamedDeclarations(isCssPropertyKnown, cssText, parseComments); 688 } 689 690 /** 691 * 692 * @returns {Array<object>} ancestorData: An array of ancestor item data 693 */ 694 _getAncestorDataForForm() { 695 const ancestorData = []; 696 697 // We don't want to compute ancestor rules for keyframe rule, as they can only be 698 // in @keyframes rules. 699 if (this.ruleClassName === "CSSKeyframeRule") { 700 return ancestorData; 701 } 702 703 // Go through all ancestor so we can build an array of all the media queries and 704 // layers this rule is in. 705 for (const ancestorRule of this.ancestorRules) { 706 const rawRule = ancestorRule.rawRule; 707 const ruleClassName = ChromeUtils.getClassName(rawRule); 708 const type = SharedCssLogic.CSSAtRuleClassNameType[ruleClassName]; 709 710 if (ruleClassName === "CSSMediaRule" && rawRule.media?.length) { 711 ancestorData.push({ 712 type, 713 value: Array.from(rawRule.media).join(", "), 714 }); 715 } else if (ruleClassName === "CSSLayerBlockRule") { 716 ancestorData.push({ 717 // we need the actorID so we can uniquely identify nameless layers on the client 718 actorID: ancestorRule.actorID, 719 type, 720 value: rawRule.name, 721 }); 722 } else if (ruleClassName === "CSSContainerRule") { 723 ancestorData.push({ 724 type, 725 // Send containerName and containerQuery separately (instead of conditionText) 726 // so the client has more flexibility to display the information. 727 containerName: rawRule.containerName, 728 containerQuery: rawRule.containerQuery, 729 }); 730 } else if (ruleClassName === "CSSSupportsRule") { 731 ancestorData.push({ 732 type, 733 conditionText: rawRule.conditionText, 734 }); 735 } else if (ruleClassName === "CSSScopeRule") { 736 ancestorData.push({ 737 type, 738 start: rawRule.start, 739 end: rawRule.end, 740 }); 741 } else if (ruleClassName === "CSSStartingStyleRule") { 742 ancestorData.push({ 743 type, 744 }); 745 } else if (rawRule.selectorText) { 746 // All the previous cases where about at-rules; this one is for regular rule 747 // that are ancestors because CSS nesting was used. 748 // In such case, we want to return the selectorText so it can be displayed in the UI. 749 const ancestor = { 750 type, 751 selectors: CssLogic.getSelectors(rawRule), 752 }; 753 754 // Only add the property when there are elements in the array to save up on serialization. 755 const selectorWarnings = rawRule.getSelectorWarnings(); 756 if (selectorWarnings.length) { 757 ancestor.selectorWarnings = selectorWarnings; 758 } 759 760 ancestorData.push(ancestor); 761 } 762 } 763 764 if (this._parentSheet) { 765 // Loop through all parent stylesheets to get the whole list of @import rules. 766 let rule = this.rawRule; 767 while ((rule = rule.parentStyleSheet?.ownerRule)) { 768 // If the rule is in a imported stylesheet with a specified layer 769 if (rule.layerName !== null) { 770 // Put the item at the top of the ancestor data array, as we're going up 771 // in the stylesheet hierarchy, and we want to display ancestor rules in the 772 // orders they're applied. 773 ancestorData.unshift({ 774 type: "layer", 775 value: rule.layerName, 776 }); 777 } 778 779 // If the rule is in a imported stylesheet with specified media/supports conditions 780 if (rule.media?.mediaText || rule.supportsText) { 781 const parts = []; 782 if (rule.supportsText) { 783 parts.push(`supports(${rule.supportsText})`); 784 } 785 786 if (rule.media?.mediaText) { 787 parts.push(rule.media.mediaText); 788 } 789 790 // Put the item at the top of the ancestor data array, as we're going up 791 // in the stylesheet hierarchy, and we want to display ancestor rules in the 792 // orders they're applied. 793 ancestorData.unshift({ 794 type: "import", 795 value: parts.join(" "), 796 }); 797 } 798 } 799 } 800 return ancestorData; 801 } 802 803 /** 804 * Send an event notifying that the location of the rule has 805 * changed. 806 * 807 * @param {number} line the new line number 808 * @param {number} column the new column number 809 */ 810 _notifyLocationChanged(line, column) { 811 this.emit("location-changed", line, column); 812 } 813 814 /** 815 * Compute the index of this actor's raw rule in its parent style 816 * sheet. The index is a vector where each element is the index of 817 * a given CSS rule in its parent. A vector is used to support 818 * nested rules. 819 */ 820 _computeRuleIndex() { 821 const index = InspectorUtils.getRuleIndex(this.rawRule); 822 this._ruleIndex = index.length ? index : null; 823 } 824 825 /** 826 * Get the rule corresponding to |this._ruleIndex| from the given 827 * style sheet. 828 * 829 * @param {DOMStyleSheet} sheet 830 * The style sheet. 831 * @return {CSSStyleRule} the rule corresponding to 832 * |this._ruleIndex| 833 */ 834 _getRuleFromIndex(parentSheet) { 835 let currentRule = null; 836 for (const i of this._ruleIndex) { 837 if (currentRule === null) { 838 currentRule = parentSheet.cssRules[i]; 839 } else { 840 currentRule = currentRule.cssRules.item(i); 841 } 842 } 843 return currentRule; 844 } 845 846 /** 847 * Called from PageStyle actor _onStylesheetUpdated. 848 */ 849 onStyleApplied(kind) { 850 if (kind === UPDATE_GENERAL) { 851 // A general change means that the rule actors are invalidated, nothing 852 // to do here. 853 return; 854 } 855 856 if (this._ruleIndex) { 857 // The sheet was updated by this actor, in a way that preserves 858 // the rules. Now, recompute our new rule from the style sheet, 859 // so that we aren't left with a reference to a dangling rule. 860 const oldRule = this.rawRule; 861 const oldActor = this.pageStyle.refMap.get(oldRule); 862 this.rawRule = this._getRuleFromIndex(this._parentSheet); 863 if (oldActor) { 864 // Also tell the page style so that future calls to _styleRef 865 // return the same StyleRuleActor. 866 this.pageStyle.updateStyleRef(oldRule, this.rawRule, this); 867 } 868 const line = InspectorUtils.getRelativeRuleLine(this.rawRule); 869 const column = InspectorUtils.getRuleColumn(this.rawRule); 870 if (line !== this.line || column !== this.column) { 871 this._notifyLocationChanged(line, column); 872 } 873 this.line = line; 874 this.column = column; 875 } 876 } 877 878 #SUPPORTED_RULES_CLASSNAMES = new Set([ 879 "CSSContainerRule", 880 "CSSKeyframeRule", 881 "CSSKeyframesRule", 882 "CSSLayerBlockRule", 883 "CSSMediaRule", 884 "CSSNestedDeclarations", 885 "CSSPositionTryRule", 886 "CSSStyleRule", 887 "CSSSupportsRule", 888 ]); 889 890 #isRuleSupported() { 891 // this.rawRule might not be an actual CSSRule (e.g. when this represent an element style), 892 // and in such case, ChromeUtils.getClassName will throw 893 try { 894 const ruleClassName = ChromeUtils.getClassName(this.rawRule); 895 return this.#SUPPORTED_RULES_CLASSNAMES.has(ruleClassName); 896 } catch (e) {} 897 898 return false; 899 } 900 901 /** 902 * Return a promise that resolves to the authored form of a rule's 903 * text, if available. If the authored form is not available, the 904 * returned promise simply resolves to the empty string. If the 905 * authored form is available, this also sets |this.authoredText|. 906 * The authored text will include invalid and otherwise ignored 907 * properties. 908 * 909 * @param {boolean} skipCache 910 * If a value for authoredText was previously found and cached, 911 * ignore it and parse the stylehseet again. The authoredText 912 * may be outdated if a descendant of this rule has changed. 913 */ 914 async getAuthoredCssText(skipCache = false) { 915 if (!this.canSetRuleText || !this.#isRuleSupported()) { 916 return ""; 917 } 918 919 if (!skipCache) { 920 if (this._failedToGetRuleText) { 921 return ""; 922 } 923 if (typeof this.authoredText === "string") { 924 return this.authoredText; 925 } 926 } 927 928 try { 929 if (this.ruleClassName == "CSSNestedDeclarations") { 930 throw new Error("getRuleText doesn't deal well with bare declarations"); 931 } 932 const resourceId = 933 this.pageStyle.styleSheetsManager.getStyleSheetResourceId( 934 this._parentSheet 935 ); 936 const cssText = 937 await this.pageStyle.styleSheetsManager.getText(resourceId); 938 const text = getRuleText(cssText, this.line, this.column); 939 // Cache the result on the rule actor to avoid parsing again next time 940 this._failedToGetRuleText = false; 941 this.authoredText = text; 942 } catch (e) { 943 this._failedToGetRuleText = true; 944 this.authoredText = undefined; 945 return ""; 946 } 947 return this.authoredText; 948 } 949 950 /** 951 * Return a promise that resolves to the complete cssText of the rule as authored. 952 * 953 * Unlike |getAuthoredCssText()|, which only returns the contents of the rule, this 954 * method includes the CSS selectors and at-rules (@media, @supports, @keyframes, etc.) 955 * 956 * If the rule type is unrecongized, the promise resolves to an empty string. 957 * If the rule is an element inline style, the promise resolves with the generated 958 * selector that uniquely identifies the element and with the rule body consisting of 959 * the element's style attribute. 960 * 961 * @return {string} 962 */ 963 async getRuleText() { 964 // Bail out if the rule is not supported or not an element inline style. 965 if (!this.#isRuleSupported(true) && this.type !== ELEMENT_STYLE) { 966 return ""; 967 } 968 969 let ruleBodyText; 970 let selectorText; 971 972 // For element inline styles, use the style attribute and generated unique selector. 973 if (this.type === ELEMENT_STYLE) { 974 ruleBodyText = this.rawNode.getAttribute("style"); 975 selectorText = this.metadata.selector; 976 } else { 977 // Get the rule's authored text and skip any cached value. 978 ruleBodyText = await this.getAuthoredCssText(true); 979 980 const resourceId = 981 this.pageStyle.styleSheetsManager.getStyleSheetResourceId( 982 this._parentSheet 983 ); 984 const stylesheetText = 985 await this.pageStyle.styleSheetsManager.getText(resourceId); 986 987 const [start, end] = getSelectorOffsets( 988 stylesheetText, 989 this.line, 990 this.column 991 ); 992 selectorText = stylesheetText.substring(start, end); 993 } 994 995 const text = `${selectorText} {${ruleBodyText}}`; 996 const { result } = SharedCssLogic.prettifyCSS(text); 997 return result; 998 } 999 1000 /** 1001 * Set the contents of the rule. This rewrites the rule in the 1002 * stylesheet and causes it to be re-evaluated. 1003 * 1004 * @param {string} newText 1005 * The new text of the rule 1006 * @param {Array} modifications 1007 * Array with modifications applied to the rule. Contains objects like: 1008 * { 1009 * type: "set", 1010 * index: <number>, 1011 * name: <string>, 1012 * value: <string>, 1013 * priority: <optional string> 1014 * } 1015 * or 1016 * { 1017 * type: "remove", 1018 * index: <number>, 1019 * name: <string>, 1020 * } 1021 * @returns the rule with updated properties 1022 */ 1023 async setRuleText(newText, modifications = []) { 1024 if (!this.canSetRuleText) { 1025 throw new Error("invalid call to setRuleText"); 1026 } 1027 1028 if (this.type === ELEMENT_STYLE) { 1029 // For element style rules, set the node's style attribute. 1030 this.rawNode.setAttributeDevtools("style", newText); 1031 } else { 1032 const resourceId = 1033 this.pageStyle.styleSheetsManager.getStyleSheetResourceId( 1034 this._parentSheet 1035 ); 1036 1037 const sheetText = 1038 await this.pageStyle.styleSheetsManager.getText(resourceId); 1039 const cssText = InspectorUtils.replaceBlockRuleBodyTextInStylesheet( 1040 sheetText, 1041 this.line, 1042 this.column, 1043 newText 1044 ); 1045 1046 if (typeof cssText !== "string") { 1047 throw new Error( 1048 "Error in InspectorUtils.replaceBlockRuleBodyTextInStylesheet" 1049 ); 1050 } 1051 1052 // setStyleSheetText will parse the stylesheet which can be costly, so only do it 1053 // if the text has actually changed. 1054 if (sheetText !== newText) { 1055 await this.pageStyle.styleSheetsManager.setStyleSheetText( 1056 resourceId, 1057 cssText, 1058 { kind: UPDATE_PRESERVING_RULES } 1059 ); 1060 } 1061 } 1062 1063 this.authoredText = newText; 1064 await this.updateAncestorRulesAuthoredText(); 1065 this.pageStyle.refreshObservedRules(this.ancestorRules); 1066 1067 // Add processed modifications to the _pendingDeclarationChanges array, 1068 // they will be emitted as CSS_CHANGE resources once `declarations` have 1069 // been re-computed in `form`. 1070 this._pendingDeclarationChanges.push(...modifications); 1071 1072 // Returning this updated actor over the protocol will update its corresponding front 1073 // and any references to it. 1074 return this; 1075 } 1076 1077 /** 1078 * Update the authored text of the ancestor rules. This should be called when setting 1079 * the authored text of a (nested) rule, so all the references are properly updated. 1080 */ 1081 async updateAncestorRulesAuthoredText() { 1082 return Promise.all( 1083 this.ancestorRules.map(rule => rule.getAuthoredCssText(true)) 1084 ); 1085 } 1086 1087 /** 1088 * Modify a rule's properties. Passed an array of modifications: 1089 * { 1090 * type: "set", 1091 * index: <number>, 1092 * name: <string>, 1093 * value: <string>, 1094 * priority: <optional string> 1095 * } 1096 * or 1097 * { 1098 * type: "remove", 1099 * index: <number>, 1100 * name: <string>, 1101 * } 1102 * 1103 * @returns the rule with updated properties 1104 */ 1105 modifyProperties(modifications) { 1106 // Use a fresh element for each call to this function to prevent side 1107 // effects that pop up based on property values that were already set on the 1108 // element. 1109 let document; 1110 if (this.rawNode) { 1111 document = this.rawNode.ownerDocument; 1112 } else { 1113 let parentStyleSheet = this._parentSheet; 1114 while (parentStyleSheet.ownerRule) { 1115 parentStyleSheet = parentStyleSheet.ownerRule.parentStyleSheet; 1116 } 1117 1118 document = this.getDocument(parentStyleSheet); 1119 } 1120 1121 const tempElement = document.createElementNS(XHTML_NS, "div"); 1122 1123 for (const mod of modifications) { 1124 if (mod.type === "set") { 1125 tempElement.style.setProperty(mod.name, mod.value, mod.priority || ""); 1126 this.rawStyle.setProperty( 1127 mod.name, 1128 tempElement.style.getPropertyValue(mod.name), 1129 mod.priority || "" 1130 ); 1131 } else if (mod.type === "remove" || mod.type === "disable") { 1132 this.rawStyle.removeProperty(mod.name); 1133 } 1134 } 1135 1136 this.pageStyle.refreshObservedRules(this.ancestorRules); 1137 1138 // Add processed modifications to the _pendingDeclarationChanges array, 1139 // they will be emitted as CSS_CHANGE resources once `declarations` have 1140 // been re-computed in `form`. 1141 this._pendingDeclarationChanges.push(...modifications); 1142 1143 return this; 1144 } 1145 1146 /** 1147 * Helper function for modifySelector, inserts the new 1148 * rule with the new selector into the parent style sheet and removes the 1149 * current rule. Returns the newly inserted css rule or null if the rule is 1150 * unsuccessfully inserted to the parent style sheet. 1151 * 1152 * @param {string} value 1153 * The new selector value 1154 * @param {boolean} editAuthored 1155 * True if the selector should be updated by editing the 1156 * authored text; false if the selector should be updated via 1157 * CSSOM. 1158 * 1159 * @returns {CSSRule} 1160 * The new CSS rule added 1161 */ 1162 async _addNewSelector(value, editAuthored) { 1163 const rule = this.rawRule; 1164 const parentStyleSheet = this._parentSheet; 1165 1166 // We know the selector modification is ok, so if the client asked 1167 // for the authored text to be edited, do it now. 1168 if (editAuthored) { 1169 const document = this.getDocument(this._parentSheet); 1170 try { 1171 document.querySelector(value); 1172 } catch (e) { 1173 return null; 1174 } 1175 1176 const resourceId = 1177 this.pageStyle.styleSheetsManager.getStyleSheetResourceId( 1178 this._parentSheet 1179 ); 1180 let authoredText = 1181 await this.pageStyle.styleSheetsManager.getText(resourceId); 1182 1183 const [startOffset, endOffset] = getSelectorOffsets( 1184 authoredText, 1185 this.line, 1186 this.column 1187 ); 1188 authoredText = 1189 authoredText.substring(0, startOffset) + 1190 value + 1191 authoredText.substring(endOffset); 1192 1193 await this.pageStyle.styleSheetsManager.setStyleSheetText( 1194 resourceId, 1195 authoredText, 1196 { kind: UPDATE_PRESERVING_RULES } 1197 ); 1198 } else { 1199 // We retrieve the parent of the rule, which can be a regular stylesheet, but also 1200 // another rule, in case the underlying rule is nested. 1201 // If the rule is nested in another rule, we need to use its parent rule to "edit" it. 1202 // If the rule has no parent rules, we can simply use the stylesheet. 1203 const parent = this.rawRule.parentRule || parentStyleSheet; 1204 const cssRules = parent.cssRules; 1205 const cssText = rule.cssText; 1206 const selectorText = rule.selectorText; 1207 1208 for (let i = 0; i < cssRules.length; i++) { 1209 if (rule === cssRules.item(i)) { 1210 try { 1211 // Inserts the new style rule into the current style sheet and 1212 // delete the current rule 1213 const ruleText = cssText.slice(selectorText.length).trim(); 1214 parent.insertRule(value + " " + ruleText, i); 1215 parent.deleteRule(i + 1); 1216 break; 1217 } catch (e) { 1218 // The selector could be invalid, or the rule could fail to insert. 1219 return null; 1220 } 1221 } 1222 } 1223 } 1224 1225 await this.updateAncestorRulesAuthoredText(); 1226 1227 return this._getRuleFromIndex(parentStyleSheet); 1228 } 1229 1230 /** 1231 * Take an object with instructions to modify a CSS declaration and log an object with 1232 * normalized metadata which describes the change in the context of this rule. 1233 * 1234 * @param {object} change 1235 * Data about a modification to a declaration. @see |modifyProperties()| 1236 * @param {object} newDeclarations 1237 * The current declarations array to get the latest values, names... 1238 * @param {object} oldDeclarations 1239 * The previous declarations array to use to fetch old values, names... 1240 */ 1241 logDeclarationChange(change, newDeclarations, oldDeclarations) { 1242 // Position of the declaration within its rule. 1243 const index = change.index; 1244 // Destructure properties from the previous CSS declaration at this index, if any, 1245 // to new variable names to indicate the previous state. 1246 let { 1247 value: prevValue, 1248 name: prevName, 1249 priority: prevPriority, 1250 commentOffsets, 1251 } = oldDeclarations[index] || {}; 1252 1253 const { value: currentValue, name: currentName } = 1254 newDeclarations[index] || {}; 1255 // A declaration is disabled if it has a `commentOffsets` array. 1256 // Here we type coerce the value to a boolean with double-bang (!!) 1257 const prevDisabled = !!commentOffsets; 1258 // Append the "!important" string if defined in the previous priority flag. 1259 prevValue = 1260 prevValue && prevPriority ? `${prevValue} !important` : prevValue; 1261 1262 const data = this.metadata; 1263 1264 switch (change.type) { 1265 case "set": { 1266 data.type = prevValue ? "declaration-add" : "declaration-update"; 1267 // If `change.newName` is defined, use it because the property is being renamed. 1268 // Otherwise, a new declaration is being created or the value of an existing 1269 // declaration is being updated. In that case, use the currentName computed 1270 // by the engine. 1271 const changeName = currentName || change.name; 1272 const name = change.newName ? change.newName : changeName; 1273 // Append the "!important" string if defined in the incoming priority flag. 1274 1275 const changeValue = currentValue || change.value; 1276 const newValue = change.priority 1277 ? `${changeValue} !important` 1278 : changeValue; 1279 1280 // Reuse the previous value string, when the property is renamed. 1281 // Otherwise, use the incoming value string. 1282 const value = change.newName ? prevValue : newValue; 1283 1284 data.add = [{ property: name, value, index }]; 1285 // If there is a previous value, log its removal together with the previous 1286 // property name. Using the previous name handles the case for renaming a property 1287 // and is harmless when updating an existing value (the name stays the same). 1288 if (prevValue) { 1289 data.remove = [{ property: prevName, value: prevValue, index }]; 1290 } else { 1291 data.remove = null; 1292 } 1293 1294 // When toggling a declaration from OFF to ON, if not renaming the property, 1295 // do not mark the previous declaration for removal, otherwise the add and 1296 // remove operations will cancel each other out when tracked. Tracked changes 1297 // have no context of "disabled", only "add" or remove, like diffs. 1298 if (prevDisabled && !change.newName && prevValue === newValue) { 1299 data.remove = null; 1300 } 1301 1302 break; 1303 } 1304 1305 case "remove": 1306 data.type = "declaration-remove"; 1307 data.add = null; 1308 data.remove = [{ property: change.name, value: prevValue, index }]; 1309 break; 1310 1311 case "disable": 1312 data.type = "declaration-disable"; 1313 data.add = null; 1314 data.remove = [{ property: change.name, value: prevValue, index }]; 1315 break; 1316 } 1317 1318 this.pageStyle.inspector.targetActor.emit("track-css-change", data); 1319 } 1320 1321 /** 1322 * Helper method for tracking CSS changes. Logs the change of this rule's selector as 1323 * two operations: a removal using the old selector and an addition using the new one. 1324 * 1325 * @param {string} oldSelector 1326 * This rule's previous selector. 1327 * @param {string} newSelector 1328 * This rule's new selector. 1329 */ 1330 logSelectorChange(oldSelector, newSelector) { 1331 this.pageStyle.inspector.targetActor.emit("track-css-change", { 1332 ...this.metadata, 1333 type: "selector-remove", 1334 add: null, 1335 remove: null, 1336 selector: oldSelector, 1337 }); 1338 1339 this.pageStyle.inspector.targetActor.emit("track-css-change", { 1340 ...this.metadata, 1341 type: "selector-add", 1342 add: null, 1343 remove: null, 1344 selector: newSelector, 1345 }); 1346 } 1347 1348 /** 1349 * Modify the current rule's selector by inserting a new rule with the new 1350 * selector value and removing the current rule. 1351 * 1352 * Returns information about the new rule and applied style 1353 * so that consumers can immediately display the new rule, whether or not the 1354 * selector matches the current element without having to refresh the whole 1355 * list. 1356 * 1357 * @param {DOMNode} node 1358 * The current selected element 1359 * @param {string} value 1360 * The new selector value 1361 * @param {boolean} editAuthored 1362 * True if the selector should be updated by editing the 1363 * authored text; false if the selector should be updated via 1364 * CSSOM. 1365 * @returns {Promise<object>} 1366 * Returns an object that contains the applied style properties of the 1367 * new rule and a boolean indicating whether or not the new selector 1368 * matches the current selected element 1369 */ 1370 async modifySelector(node, value, editAuthored = false) { 1371 if (this.type === ELEMENT_STYLE || this.rawRule.selectorText === value) { 1372 return { ruleProps: null, isMatching: true }; 1373 } 1374 1375 // The rule's previous selector is lost after calling _addNewSelector(). Save it now. 1376 const oldValue = this.rawRule.selectorText; 1377 const newCssRule = await this._addNewSelector(value, editAuthored); 1378 1379 if (editAuthored && newCssRule) { 1380 this.logSelectorChange(oldValue, value); 1381 const style = this.pageStyle.styleRef(newCssRule); 1382 // See the comment in |form| to understand this. 1383 await style.getAuthoredCssText(); 1384 } 1385 1386 let entries = null; 1387 let isMatching = false; 1388 1389 if (newCssRule) { 1390 const ruleEntry = this.pageStyle.findEntryMatchingRule(node, newCssRule); 1391 if (ruleEntry) { 1392 entries = this.pageStyle.getAppliedProps(node, [ruleEntry], { 1393 matchedSelectors: true, 1394 }); 1395 } else { 1396 entries = this.pageStyle.getNewAppliedProps(node, newCssRule); 1397 } 1398 1399 isMatching = entries.some( 1400 ruleProp => !!ruleProp.matchedSelectorIndexes.length 1401 ); 1402 } 1403 1404 const result = { isMatching }; 1405 if (entries) { 1406 result.ruleProps = { entries }; 1407 } 1408 1409 return result; 1410 } 1411 1412 /** 1413 * Get the eligible query container for a given @container rule and a given node 1414 * 1415 * @param {number} ancestorRuleIndex: The index of the @container rule in this.ancestorRules 1416 * @param {NodeActor} nodeActor: The nodeActor for which we want to retrieve the query container 1417 * @returns {object} An object with the following properties: 1418 * - node: {NodeActor|null} The nodeActor representing the query container, 1419 * null if none were found 1420 * - containerType: {string} The computed `containerType` value of the query container 1421 * - inlineSize: {string} The computed `inlineSize` value of the query container (e.g. `120px`) 1422 * - blockSize: {string} The computed `blockSize` value of the query container (e.g. `812px`) 1423 */ 1424 getQueryContainerForNode(ancestorRuleIndex, nodeActor) { 1425 const ancestorRule = this.ancestorRules[ancestorRuleIndex]; 1426 if (!ancestorRule) { 1427 console.error( 1428 `Couldn't not find an ancestor rule at index ${ancestorRuleIndex}` 1429 ); 1430 return { node: null }; 1431 } 1432 1433 const containerEl = ancestorRule.rawRule.queryContainerFor( 1434 nodeActor.rawNode 1435 ); 1436 1437 // queryContainerFor returns null when the container name wasn't find in any ancestor. 1438 // In practice this shouldn't happen, as if the rule is applied, it means that an 1439 // elligible container was found. 1440 if (!containerEl) { 1441 return { node: null }; 1442 } 1443 1444 const computedStyle = CssLogic.getComputedStyle(containerEl); 1445 return { 1446 node: this.pageStyle.walker.getNode(containerEl), 1447 containerType: computedStyle.containerType, 1448 inlineSize: computedStyle.inlineSize, 1449 blockSize: computedStyle.blockSize, 1450 }; 1451 } 1452 1453 /** 1454 * Using the latest computed style applicable to the selected element, 1455 * check the states of declarations in this CSS rule. 1456 * 1457 * If any have changed their used/unused state, potentially as a result of changes in 1458 * another rule, fire a "rule-updated" event with this rule actor in its latest state. 1459 * 1460 * @param {boolean} forceRefresh: Set to true to emit "rule-updated", even if the state 1461 * of the declarations didn't change. 1462 */ 1463 maybeRefresh(forceRefresh) { 1464 let hasChanged = false; 1465 1466 const el = this.currentlySelectedElement; 1467 const style = this.currentlySelectedElementComputedStyle; 1468 1469 for (const decl of this._declarations) { 1470 const inactiveCssData = getInactiveCssDataForProperty( 1471 el, 1472 style, 1473 this.rawRule, 1474 decl.name 1475 ); 1476 1477 if (!decl.inactiveCssData !== !inactiveCssData) { 1478 if (inactiveCssData) { 1479 decl.inactiveCssData = inactiveCssData; 1480 } else { 1481 delete decl.inactiveCssData; 1482 } 1483 hasChanged = true; 1484 } 1485 } 1486 1487 if (hasChanged || forceRefresh) { 1488 // ⚠️ IMPORTANT ⚠️ 1489 // When an event is emitted via the protocol with the StyleRuleActor as payload, the 1490 // corresponding StyleRuleFront will be automatically updated under the hood. 1491 // Therefore, when the client looks up properties on the front reference it already 1492 // has, it will get the latest values set on the actor, not the ones it originally 1493 // had when the front was created. The client is not required to explicitly replace 1494 // its previous front reference to the one it receives as this event's payload. 1495 // The client doesn't even need to explicitly listen for this event. 1496 // The update of the front happens automatically. 1497 this.emit("rule-updated", this); 1498 } 1499 } 1500 } 1501 exports.StyleRuleActor = StyleRuleActor; 1502 1503 /** 1504 * Compute the start and end offsets of a rule's selector text, given 1505 * the CSS text and the line and column at which the rule begins. 1506 * 1507 * @param {string} initialText 1508 * @param {number} line (1-indexed) 1509 * @param {number} column (1-indexed) 1510 * @return {Array} An array with two elements: [startOffset, endOffset]. 1511 * The elements mark the bounds in |initialText| of 1512 * the CSS rule's selector. 1513 */ 1514 function getSelectorOffsets(initialText, line, column) { 1515 if (typeof line === "undefined" || typeof column === "undefined") { 1516 throw new Error("Location information is missing"); 1517 } 1518 1519 const { offset: textOffset, text } = getTextAtLineColumn( 1520 initialText, 1521 line, 1522 column 1523 ); 1524 const lexer = new InspectorCSSParserWrapper(text); 1525 1526 // Search forward for the opening brace. 1527 let endOffset; 1528 let token; 1529 while ((token = lexer.nextToken())) { 1530 if (token.tokenType === "CurlyBracketBlock") { 1531 if (endOffset === undefined) { 1532 break; 1533 } 1534 return [textOffset, textOffset + endOffset]; 1535 } 1536 // Preserve comments and whitespace just before the "{". 1537 if (token.tokenType !== "Comment" && token.tokenType !== "WhiteSpace") { 1538 endOffset = token.endOffset; 1539 } 1540 } 1541 1542 throw new Error("could not find bounds of rule"); 1543 }