tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }