tor-browser

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

page-style.js (51846B)


      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  pageStyleSpec,
     10 } = require("resource://devtools/shared/specs/page-style.js");
     11 
     12 const {
     13  LongStringActor,
     14 } = require("resource://devtools/server/actors/string.js");
     15 
     16 const {
     17  style: { ELEMENT_STYLE },
     18 } = require("resource://devtools/shared/constants.js");
     19 
     20 loader.lazyRequireGetter(
     21  this,
     22  "StyleRuleActor",
     23  "resource://devtools/server/actors/style-rule.js",
     24  true
     25 );
     26 loader.lazyRequireGetter(
     27  this,
     28  "getFontPreviewData",
     29  "resource://devtools/server/actors/utils/style-utils.js",
     30  true
     31 );
     32 loader.lazyRequireGetter(
     33  this,
     34  "CssLogic",
     35  "resource://devtools/server/actors/inspector/css-logic.js",
     36  true
     37 );
     38 loader.lazyRequireGetter(
     39  this,
     40  "SharedCssLogic",
     41  "resource://devtools/shared/inspector/css-logic.js"
     42 );
     43 loader.lazyRequireGetter(
     44  this,
     45  "getDefinedGeometryProperties",
     46  "resource://devtools/server/actors/highlighters/geometry-editor.js",
     47  true
     48 );
     49 loader.lazyRequireGetter(
     50  this,
     51  "UPDATE_GENERAL",
     52  "resource://devtools/server/actors/utils/stylesheets-manager.js",
     53  true
     54 );
     55 
     56 loader.lazyGetter(this, "PSEUDO_ELEMENTS", () => {
     57  return InspectorUtils.getCSSPseudoElementNames();
     58 });
     59 loader.lazyGetter(this, "FONT_VARIATIONS_ENABLED", () => {
     60  return Services.prefs.getBoolPref("layout.css.font-variations.enabled");
     61 });
     62 
     63 const NORMAL_FONT_WEIGHT = 400;
     64 const BOLD_FONT_WEIGHT = 700;
     65 
     66 /**
     67 * The PageStyle actor lets the client look at the styles on a page, as
     68 * they are applied to a given node.
     69 */
     70 class PageStyleActor extends Actor {
     71  /**
     72   * Create a PageStyleActor.
     73   *
     74   * @param inspector
     75   *    The InspectorActor that owns this PageStyleActor.
     76   *
     77   * @class
     78   */
     79  constructor(inspector) {
     80    super(inspector.conn, pageStyleSpec);
     81    this.inspector = inspector;
     82    if (!this.inspector.walker) {
     83      throw Error(
     84        "The inspector's WalkerActor must be created before " +
     85          "creating a PageStyleActor."
     86      );
     87    }
     88    this.walker = inspector.walker;
     89    this.cssLogic = new CssLogic();
     90 
     91    // Stores the association of DOM objects -> actors
     92    this.refMap = new Map();
     93 
     94    // Latest node queried for its applied styles.
     95    this.selectedElement = null;
     96 
     97    // Maps root node (document|ShadowRoot) to stylesheets, which are used to add new rules.
     98    this.styleSheetsByRootNode = new WeakMap();
     99 
    100    this.onFrameUnload = this.onFrameUnload.bind(this);
    101 
    102    this.inspector.targetActor.on("will-navigate", this.onFrameUnload);
    103 
    104    this.styleSheetsManager =
    105      this.inspector.targetActor.getStyleSheetsManager();
    106 
    107    this.styleSheetsManager.on("stylesheet-updated", this.#onStylesheetUpdated);
    108  }
    109 
    110  #observedRules = new Set();
    111 
    112  destroy() {
    113    if (!this.walker) {
    114      return;
    115    }
    116    super.destroy();
    117    this.inspector.targetActor.off("will-navigate", this.onFrameUnload);
    118    this.inspector = null;
    119    this.walker = null;
    120    this.refMap = null;
    121    this.selectedElement = null;
    122    this.cssLogic = null;
    123    this.styleSheetsByRootNode = null;
    124 
    125    this.#observedRules = null;
    126  }
    127 
    128  get ownerWindow() {
    129    return this.inspector.targetActor.window;
    130  }
    131 
    132  form() {
    133    // We need to use CSS from the inspected window in order to use CSS.supports() and
    134    // detect the right platform features from there.
    135    const CSS = this.inspector.targetActor.window.CSS;
    136 
    137    return {
    138      actor: this.actorID,
    139      traits: {
    140        // Whether the page supports values of font-stretch from CSS Fonts Level 4.
    141        fontStretchLevel4: CSS.supports("font-stretch: 100%"),
    142        // Whether the page supports values of font-style from CSS Fonts Level 4.
    143        fontStyleLevel4: CSS.supports("font-style: oblique 20deg"),
    144        // Whether getAllUsedFontFaces/getUsedFontFaces accepts the includeVariations
    145        // argument.
    146        fontVariations: FONT_VARIATIONS_ENABLED,
    147        // Whether the page supports values of font-weight from CSS Fonts Level 4.
    148        // font-weight at CSS Fonts Level 4 accepts values in increments of 1 rather
    149        // than 100. However, CSS.supports() returns false positives, so we guard with the
    150        // expected support of font-stretch at CSS Fonts Level 4.
    151        fontWeightLevel4:
    152          CSS.supports("font-weight: 1") && CSS.supports("font-stretch: 100%"),
    153      },
    154    };
    155  }
    156 
    157  /**
    158   * Called when a style sheet is updated.
    159   */
    160  #styleApplied = kind => {
    161    // No matter what kind of update is done, we need to invalidate
    162    // the keyframe cache.
    163    this.cssLogic.reset();
    164    if (kind === UPDATE_GENERAL) {
    165      this.emit("stylesheet-updated");
    166    }
    167  };
    168 
    169  /**
    170   * Return or create a StyleRuleActor for the given item.
    171   *
    172   * @param {CSSStyleRule|Element} item
    173   * @param {string} pseudoElement An optional pseudo-element type in cases when the CSS
    174   *        rule applies to a pseudo-element.
    175   * @param {boolean} userAdded: Optional boolean to distinguish rules added by the user.
    176   * @return {StyleRuleActor} The newly created, or cached, StyleRuleActor for this item.
    177   */
    178  styleRef(item, pseudoElement, userAdded = false) {
    179    if (this.refMap.has(item)) {
    180      const styleRuleActor = this.refMap.get(item);
    181      if (pseudoElement) {
    182        styleRuleActor.addPseudo(pseudoElement);
    183      }
    184      return styleRuleActor;
    185    }
    186    const actor = new StyleRuleActor({
    187      pageStyle: this,
    188      item,
    189      userAdded,
    190      pseudoElement,
    191    });
    192    this.manage(actor);
    193    this.refMap.set(item, actor);
    194 
    195    return actor;
    196  }
    197 
    198  /**
    199   * Update the association between a StyleRuleActor and its
    200   * corresponding item.  This is used when a StyleRuleActor updates
    201   * as style sheet and starts using a new rule.
    202   *
    203   * @param oldItem The old association; either a CSSStyleRule or a
    204   *                DOM element.
    205   * @param item Either a CSSStyleRule or a DOM element.
    206   * @param actor a StyleRuleActor
    207   */
    208  updateStyleRef(oldItem, item, actor) {
    209    this.refMap.delete(oldItem);
    210    this.refMap.set(item, actor);
    211  }
    212 
    213  /**
    214   * Get the StyleRuleActor matching the given rule id or null if no match is found.
    215   *
    216   * @param  {string} ruleId
    217   *         Actor ID of the StyleRuleActor
    218   * @return {StyleRuleActor|null}
    219   */
    220  getRule(ruleId) {
    221    let match = null;
    222 
    223    for (const actor of this.refMap.values()) {
    224      if (actor.actorID === ruleId) {
    225        match = actor;
    226        continue;
    227      }
    228    }
    229 
    230    return match;
    231  }
    232 
    233  /**
    234   * Get the computed style for a node.
    235   *
    236   * @param {NodeActor} node
    237   * @param {object} options
    238   * @param {string} options.filter: A string filter that affects the "matched" handling.
    239   * @param {Array<string>} options.filterProperties: An array of properties names that
    240   *        you would like returned.
    241   * @param {boolean} options.markMatched: true if you want the 'matched' property to be
    242   *        added when a computed property has been modified by a style included by `filter`.
    243   * @param {boolean} options.onlyMatched: true if unmatched properties shouldn't be included.
    244   * @param {boolean} options.clearCache: true if the cssLogic cache should be cleared.
    245   *
    246   * @returns a JSON blob with the following form:
    247   *   {
    248   *     "property-name": {
    249   *       value: "property-value",
    250   *       priority: "!important" <optional>
    251   *       matched: <true if there are matched selectors for this value>
    252   *     },
    253   *     ...
    254   *   }
    255   */
    256  getComputed(node, options) {
    257    const ret = Object.create(null);
    258 
    259    if (options.clearCache) {
    260      this.cssLogic.reset();
    261    }
    262    const filterProperties = Array.isArray(options.filterProperties)
    263      ? options.filterProperties
    264      : null;
    265    this.cssLogic.sourceFilter = options.filter || SharedCssLogic.FILTER.UA;
    266    this.cssLogic.highlight(node.rawNode);
    267    const computed = this.cssLogic.computedStyle || [];
    268    const targetDocument = this.inspector.targetActor.window.document;
    269 
    270    for (const name of computed) {
    271      if (filterProperties && !filterProperties.includes(name)) {
    272        continue;
    273      }
    274      ret[name] = {
    275        value: computed.getPropertyValue(name),
    276        priority: computed.getPropertyPriority(name) || undefined,
    277      };
    278 
    279      if (name.startsWith("--")) {
    280        const registeredProperty = InspectorUtils.getCSSRegisteredProperty(
    281          targetDocument,
    282          name
    283        );
    284        if (registeredProperty) {
    285          ret[name].registeredPropertyInitialValue =
    286            registeredProperty.initialValue;
    287          if (
    288            !InspectorUtils.valueMatchesSyntax(
    289              targetDocument,
    290              ret[name].value,
    291              registeredProperty.syntax
    292            )
    293          ) {
    294            ret[name].invalidAtComputedValueTime = true;
    295            ret[name].registeredPropertySyntax = registeredProperty.syntax;
    296          }
    297        }
    298      }
    299    }
    300 
    301    if (options.markMatched || options.onlyMatched) {
    302      const matched = this.cssLogic.hasMatchedSelectors(Object.keys(ret));
    303      for (const key in ret) {
    304        if (matched.has(key)) {
    305          ret[key].matched = options.markMatched ? true : undefined;
    306        } else if (options.onlyMatched) {
    307          delete ret[key];
    308        }
    309      }
    310    }
    311 
    312    return ret;
    313  }
    314 
    315  /**
    316   * Get all the fonts from a page.
    317   *
    318   * @param object options
    319   *   `includePreviews`: Whether to also return image previews of the fonts.
    320   *   `previewText`: The text to display in the previews.
    321   *   `previewFontSize`: The font size of the text in the previews.
    322   *
    323   * @returns object
    324   *   object with 'fontFaces', a list of fonts that apply to this node.
    325   */
    326  getAllUsedFontFaces(options) {
    327    const windows = this.inspector.targetActor.windows;
    328    let fontsList = [];
    329    for (const win of windows) {
    330      // Fall back to the documentElement for XUL documents.
    331      const node = win.document.body
    332        ? win.document.body
    333        : win.document.documentElement;
    334      fontsList = [...fontsList, ...this.getUsedFontFaces(node, options)];
    335    }
    336 
    337    return fontsList;
    338  }
    339 
    340  /**
    341   * Get the font faces used in an element.
    342   *
    343   * @param NodeActor node / actual DOM node
    344   *    The node to get fonts from.
    345   * @param object options
    346   *   `includePreviews`: Whether to also return image previews of the fonts.
    347   *   `previewText`: The text to display in the previews.
    348   *   `previewFontSize`: The font size of the text in the previews.
    349   *
    350   * @returns object
    351   *   object with 'fontFaces', a list of fonts that apply to this node.
    352   */
    353  getUsedFontFaces(node, options) {
    354    // node.rawNode is defined for NodeActor objects
    355    const actualNode = node.rawNode || node;
    356    const contentDocument = actualNode.ownerDocument;
    357    // We don't get fonts for a node, but for a range
    358    const rng = contentDocument.createRange();
    359    const isPseudoElement = Boolean(
    360      CssLogic.getBindingElementAndPseudo(actualNode).pseudo
    361    );
    362    if (isPseudoElement) {
    363      rng.selectNodeContents(actualNode);
    364    } else {
    365      rng.selectNode(actualNode);
    366    }
    367    const fonts = InspectorUtils.getUsedFontFaces(rng);
    368    const fontsArray = [];
    369 
    370    for (let i = 0; i < fonts.length; i++) {
    371      const font = fonts[i];
    372      const fontFace = {
    373        name: font.name,
    374        CSSFamilyName: font.CSSFamilyName,
    375        CSSGeneric: font.CSSGeneric || null,
    376        srcIndex: font.srcIndex,
    377        URI: font.URI,
    378        format: font.format,
    379        localName: font.localName,
    380        metadata: font.metadata,
    381        version: font.getNameString(InspectorFontFace.NAME_ID_VERSION),
    382        description: font.getNameString(InspectorFontFace.NAME_ID_DESCRIPTION),
    383        manufacturer: font.getNameString(
    384          InspectorFontFace.NAME_ID_MANUFACTURER
    385        ),
    386        vendorUrl: font.getNameString(InspectorFontFace.NAME_ID_VENDOR_URL),
    387        designer: font.getNameString(InspectorFontFace.NAME_ID_DESIGNER),
    388        designerUrl: font.getNameString(InspectorFontFace.NAME_ID_DESIGNER_URL),
    389        license: font.getNameString(InspectorFontFace.NAME_ID_LICENSE),
    390        licenseUrl: font.getNameString(InspectorFontFace.NAME_ID_LICENSE_URL),
    391        sampleText: font.getNameString(InspectorFontFace.NAME_ID_SAMPLE_TEXT),
    392      };
    393 
    394      // If this font comes from a @font-face rule
    395      if (font.rule) {
    396        const styleActor = new StyleRuleActor({
    397          pageStyle: this,
    398          item: font.rule,
    399        });
    400        this.manage(styleActor);
    401        fontFace.rule = styleActor;
    402        fontFace.ruleText = font.rule.cssText;
    403      }
    404 
    405      // Get the weight and style of this font for the preview and sort order
    406      let weight = NORMAL_FONT_WEIGHT,
    407        style = "";
    408      if (font.rule) {
    409        weight =
    410          font.rule.style.getPropertyValue("font-weight") || NORMAL_FONT_WEIGHT;
    411        if (weight == "bold") {
    412          weight = BOLD_FONT_WEIGHT;
    413        } else if (weight == "normal") {
    414          weight = NORMAL_FONT_WEIGHT;
    415        }
    416        style = font.rule.style.getPropertyValue("font-style") || "";
    417      }
    418      fontFace.weight = weight;
    419      fontFace.style = style;
    420 
    421      if (options.includePreviews) {
    422        const opts = {
    423          previewText: options.previewText,
    424          previewFontSize: options.previewFontSize,
    425          fontStyle: style,
    426          fontWeight: weight,
    427          fillStyle: options.previewFillStyle,
    428        };
    429        const { dataURL, size } = getFontPreviewData(
    430          font.CSSFamilyName,
    431          contentDocument,
    432          opts
    433        );
    434        fontFace.preview = {
    435          data: new LongStringActor(this.conn, dataURL),
    436          size,
    437        };
    438      }
    439 
    440      if (options.includeVariations && FONT_VARIATIONS_ENABLED) {
    441        fontFace.variationAxes = font.getVariationAxes();
    442        fontFace.variationInstances = font.getVariationInstances();
    443      }
    444 
    445      fontsArray.push(fontFace);
    446    }
    447 
    448    // @font-face fonts at the top, then alphabetically, then by weight
    449    fontsArray.sort(function (a, b) {
    450      return a.weight > b.weight ? 1 : -1;
    451    });
    452    fontsArray.sort(function (a, b) {
    453      if (a.CSSFamilyName == b.CSSFamilyName) {
    454        return 0;
    455      }
    456      return a.CSSFamilyName > b.CSSFamilyName ? 1 : -1;
    457    });
    458    fontsArray.sort(function (a, b) {
    459      if ((a.rule && b.rule) || (!a.rule && !b.rule)) {
    460        return 0;
    461      }
    462      return !a.rule && b.rule ? 1 : -1;
    463    });
    464 
    465    return fontsArray;
    466  }
    467 
    468  /**
    469   * Get a list of selectors that match a given property for a node.
    470   *
    471   * @param NodeActor node
    472   * @param string property
    473   * @param object options
    474   *   `filter`: A string filter that affects the "matched" handling.
    475   *     'user': Include properties from user style sheets.
    476   *     'ua': Include properties from user and user-agent sheets.
    477   *     Default value is 'ua'
    478   *
    479   * @returns a JSON object with the following form:
    480   *   {
    481   *     // An ordered list of rules that apply
    482   *     matched: [{
    483   *       rule: <rule actorid>,
    484   *       sourceText: <string>, // The source of the selector, relative
    485   *                             // to the node in question.
    486   *       selector: <string>, // the selector ID that matched
    487   *       value: <string>, // the value of the property
    488   *       status: <int>,
    489   *         // The status of the match - high numbers are better placed
    490   *         // to provide styling information:
    491   *         // 3: Best match, was used.
    492   *         // 2: Matched, but was overridden.
    493   *         // 1: Rule from a parent matched.
    494   *         // 0: Unmatched (never returned in this API)
    495   *     }, ...],
    496   *
    497   *     // The full form of any domrule referenced.
    498   *     rules: [ <domrule>, ... ], // The full form of any domrule referenced
    499   *
    500   *     // The full form of any sheets referenced.
    501   *     sheets: [ <domsheet>, ... ]
    502   *  }
    503   */
    504  getMatchedSelectors(node, property, options) {
    505    this.cssLogic.sourceFilter = options.filter || SharedCssLogic.FILTER.UA;
    506    this.cssLogic.highlight(node.rawNode);
    507 
    508    const rules = new Set();
    509    const matched = [];
    510 
    511    const targetDocument = this.inspector.targetActor.window.document;
    512    let registeredProperty;
    513    if (property.startsWith("--")) {
    514      registeredProperty = InspectorUtils.getCSSRegisteredProperty(
    515        targetDocument,
    516        property
    517      );
    518    }
    519 
    520    const propInfo = this.cssLogic.getPropertyInfo(property);
    521    for (const selectorInfo of propInfo.matchedSelectors) {
    522      const cssRule = selectorInfo.selector.cssRule;
    523      const domRule = cssRule.sourceElement || cssRule.domRule;
    524 
    525      const rule = this.styleRef(domRule);
    526      rules.add(rule);
    527 
    528      const match = {
    529        rule,
    530        sourceText: this.getSelectorSource(selectorInfo, node.rawNode),
    531        selector: selectorInfo.selector.text,
    532        name: selectorInfo.property,
    533        value: selectorInfo.value,
    534        status: selectorInfo.status,
    535      };
    536      if (
    537        registeredProperty &&
    538        !InspectorUtils.valueMatchesSyntax(
    539          targetDocument,
    540          match.value,
    541          registeredProperty.syntax
    542        )
    543      ) {
    544        match.invalidAtComputedValueTime = true;
    545        match.registeredPropertySyntax = registeredProperty.syntax;
    546      }
    547      matched.push(match);
    548    }
    549 
    550    return {
    551      matched,
    552      rules: [...rules],
    553    };
    554  }
    555 
    556  // Get a selector source for a CssSelectorInfo relative to a given
    557  // node.
    558  getSelectorSource(selectorInfo, relativeTo) {
    559    let result = selectorInfo.selector.text;
    560    const ruleDeclarationOrigin =
    561      selectorInfo.selector.cssRule.domRule.declarationOrigin;
    562    if (
    563      ruleDeclarationOrigin === "style-attribute" ||
    564      ruleDeclarationOrigin === "pres-hints"
    565    ) {
    566      const source = selectorInfo.sourceElement;
    567      if (source === relativeTo) {
    568        result = "element";
    569      } else {
    570        result = CssLogic.getShortName(source);
    571      }
    572 
    573      if (ruleDeclarationOrigin === "pres-hints") {
    574        result += " attributes style";
    575      }
    576    }
    577 
    578    return result;
    579  }
    580 
    581  /**
    582   * @typedef {"user" | "ua" } GetAppliedFilterOption
    583   */
    584 
    585  /**
    586   * @typedef {object} GetAppliedOptions
    587   *
    588   * @property {GetAppliedFilterOption} filter - A string filter that affects the "matched" handling.
    589   *        Possible values are:
    590   *        - 'user': Include properties from user style sheets.
    591   *        - 'ua': Include properties from user and user-agent sheets.
    592   *        Default value is 'ua'
    593   * @property {boolean} inherited - Include styles inherited from parent nodes.
    594   * @property {boolean} matchedSelectors - Include an array of specific selectors that
    595   *        caused this rule to match its node.
    596   * @property {boolean} skipPseudo - Exclude styles applied to pseudo elements of the
    597   *        provided node.
    598   */
    599 
    600  /**
    601   * Get the set of styles that apply to a given node.
    602   *
    603   * @param {NodeActor} node
    604   * @param {GetAppliedOptions} options
    605   */
    606  async getApplied(node, options) {
    607    // Clear any previous references to StyleRuleActor instances for CSS rules.
    608    // Assume the consumer has switched context to a new node and no longer
    609    // interested in state changes of previous rules.
    610    this.#observedRules.clear();
    611    this.selectedElement = node?.rawNode || null;
    612 
    613    if (!node) {
    614      return { entries: [] };
    615    }
    616 
    617    this.cssLogic.highlight(node.rawNode);
    618 
    619    const entries = this.getAppliedProps(
    620      node,
    621      this.#getAllElementRules(node, {
    622        skipPseudo: options.skipPseudo,
    623        filter: options.filter,
    624      }),
    625      options
    626    );
    627 
    628    const promises = [];
    629    for (const entry of entries) {
    630      // Reference to instances of StyleRuleActor for CSS rules matching the node.
    631      // Assume these are used by a consumer which wants to be notified when their
    632      // state or declarations change either directly or indirectly.
    633      this.#observedRules.add(entry.rule);
    634      // We need to be sure that authoredText has been set before StyleRule#form is called.
    635      // This has to be treated specially, for now, because we cannot synchronously compute
    636      // the authored text and |form| can't return a promise.
    637      // See bug 1205868.
    638      promises.push(entry.rule.getAuthoredCssText());
    639    }
    640 
    641    await Promise.all(promises);
    642 
    643    return { entries };
    644  }
    645 
    646  #hasInheritedProps(style) {
    647    const doc = this.inspector.targetActor.window.document;
    648    return Array.prototype.some.call(style, prop =>
    649      InspectorUtils.isInheritedProperty(doc, prop)
    650    );
    651  }
    652 
    653  async isPositionEditable(node) {
    654    if (!node || node.rawNode.nodeType !== node.rawNode.ELEMENT_NODE) {
    655      return false;
    656    }
    657 
    658    const props = getDefinedGeometryProperties(node.rawNode);
    659 
    660    // Elements with only `width` and `height` are currently not considered
    661    // editable.
    662    return (
    663      props.has("top") ||
    664      props.has("right") ||
    665      props.has("left") ||
    666      props.has("bottom")
    667    );
    668  }
    669 
    670  /**
    671   * Helper function for getApplied, gets all the rules from a given
    672   * element. See getApplied for documentation on parameters.
    673   *
    674   * @param {NodeActor} node
    675   * @param {object} options
    676   * @param {boolean} options.isInherited - Set to true if we want to retrieve inherited rules,
    677   *        i.e. the passed node actor is an ancestor of the node we want to retrieved the
    678   *        applied rules for originally.
    679   * @param {boolean} options.skipPseudo - Exclude styles applied to pseudo elements of the
    680   *        provided node
    681   * @param {GetAppliedFilterOption} options.filter - will be passed to #getElementRules
    682   *
    683   * @return Array The rules for a given element. Each item in the
    684   *               array has the following signature:
    685   *                - rule RuleActor
    686   *                - inherited NodeActor
    687   *                - isSystem Boolean
    688   *                - pseudoElement String
    689   *                - darkColorScheme Boolean
    690   */
    691  #getAllElementRules(node, { isInherited, skipPseudo, filter }) {
    692    const { bindingElement, pseudo } = CssLogic.getBindingElementAndPseudo(
    693      node.rawNode
    694    );
    695    const rules = [];
    696 
    697    if (!bindingElement) {
    698      return rules;
    699    }
    700 
    701    if (bindingElement.style) {
    702      const elementStyle = this.styleRef(
    703        bindingElement,
    704        // for inline style, we can't have a related pseudo element
    705        null
    706      );
    707      const showElementStyles = !isInherited && !pseudo;
    708      const showInheritedStyles =
    709        isInherited && this.#hasInheritedProps(bindingElement.style);
    710 
    711      const rule = this.#getRuleItem(elementStyle, node.rawNode, {
    712        pseudoElement: null,
    713        isSystem: false,
    714        inherited: null,
    715      });
    716 
    717      // First any inline styles
    718      if (showElementStyles) {
    719        rules.push(rule);
    720      }
    721 
    722      // Now any inherited styles
    723      if (showInheritedStyles) {
    724        // at this point `isInherited` is true, so we want to put the NodeActor in the
    725        // `inherited` property so the client can show this information (for example in
    726        // the "Inherited from X" section in the Rules view).
    727        rule.inherited = node;
    728        rules.push(rule);
    729      }
    730    }
    731 
    732    // Add normal rules.  Typically this is passing in the node passed into the
    733    // function, unless if that node was ::before/::after.  In which case,
    734    // it will pass in the parentNode along with "::before"/"::after".
    735    this.#getElementRules(
    736      bindingElement,
    737      pseudo,
    738      isInherited ? node : null,
    739      filter
    740    ).forEach(oneRule => {
    741      // The only case when there would be a pseudo here is
    742      // ::before/::after, and in this case we want to tell the
    743      // view that it belongs to the element (which is a
    744      // _moz_generated_content native anonymous element).
    745      oneRule.pseudoElement = null;
    746      rules.push(oneRule);
    747    });
    748 
    749    // If we don't want to check pseudo elements rules, we can stop here.
    750    if (skipPseudo) {
    751      return rules;
    752    }
    753 
    754    // Now retrieve any pseudo element rules.
    755    // We can have pseudo element that are children of other pseudo elements (e.g. with
    756    // ::before::marker , ::marker is a child of ::before).
    757    // In such case, we want to call #getElementRules with the actual pseudo element node,
    758    // not its binding element.
    759    const elementForPseudo = pseudo ? node.rawNode : bindingElement;
    760 
    761    const relevantPseudoElements = [];
    762    for (const readPseudo of PSEUDO_ELEMENTS) {
    763      if (!this.#pseudoIsRelevant(elementForPseudo, readPseudo, isInherited)) {
    764        continue;
    765      }
    766 
    767      // FIXME: Bug 1909173. Need to handle view transitions peudo-elements.
    768      if (readPseudo === "::highlight") {
    769        InspectorUtils.getRegisteredCssHighlights(
    770          this.inspector.targetActor.window.document,
    771          // only active
    772          true
    773        ).forEach(name => {
    774          relevantPseudoElements.push(`::highlight(${name})`);
    775        });
    776      } else {
    777        relevantPseudoElements.push(readPseudo);
    778      }
    779    }
    780 
    781    for (const readPseudo of relevantPseudoElements) {
    782      const pseudoRules = this.#getElementRules(
    783        elementForPseudo,
    784        readPseudo,
    785        isInherited ? node : null,
    786        filter
    787      );
    788      // inherited element backed pseudo element rules (e.g. `::details-content`) should
    789      // not be at the same "level" as rules inherited from the binding element (e.g. `<details>`),
    790      // so we need to put them before the "regular" rules.
    791      if (
    792        SharedCssLogic.ELEMENT_BACKED_PSEUDO_ELEMENTS.has(readPseudo) &&
    793        isInherited
    794      ) {
    795        rules.unshift(...pseudoRules);
    796      } else {
    797        rules.push(...pseudoRules);
    798      }
    799    }
    800 
    801    return rules;
    802  }
    803 
    804  /**
    805   * @param {DOMNode} rawNode
    806   * @param {StyleRuleActor} styleRuleActor
    807   * @param {object} params
    808   * @param {NodeActor} params.inherited
    809   * @param {boolean} params.isSystem
    810   * @param {string | null} params.pseudoElement
    811   * @returns Object
    812   */
    813  #getRuleItem(rule, rawNode, { inherited, isSystem, pseudoElement }) {
    814    return {
    815      rule,
    816      pseudoElement,
    817      isSystem,
    818      inherited,
    819      // We can't compute the value for the whole document as the color scheme
    820      // can be set at the node level (e.g. with `color-scheme`)
    821      darkColorScheme: InspectorUtils.isUsedColorSchemeDark(rawNode),
    822    };
    823  }
    824 
    825  #nodeIsTextfieldLike(node) {
    826    if (node.nodeName == "TEXTAREA") {
    827      return true;
    828    }
    829    return (
    830      node.mozIsTextField &&
    831      (node.mozIsTextField(false) || node.type == "number")
    832    );
    833  }
    834 
    835  #nodeIsListItem(node) {
    836    const computed = CssLogic.getComputedStyle(node);
    837    if (!computed) {
    838      return false;
    839    }
    840 
    841    const display = computed.getPropertyValue("display");
    842    // This is written this way to handle `inline list-item` and such.
    843    return display.split(" ").includes("list-item");
    844  }
    845 
    846  /**
    847   * Returns whether or node the pseudo element is relevant for the passed node
    848   *
    849   * @param {DOMNode} node
    850   * @param {string} pseudo
    851   * @param {boolean} isInherited
    852   * @returns {boolean}
    853   */
    854  // eslint-disable-next-line complexity
    855  #pseudoIsRelevant(node, pseudo, isInherited = false) {
    856    switch (pseudo) {
    857      case "::after":
    858      case "::before":
    859      case "::first-letter":
    860      case "::first-line":
    861      case "::selection":
    862      case "::highlight":
    863      case "::target-text":
    864        return !isInherited;
    865      case "::marker":
    866        return !isInherited && this.#nodeIsListItem(node);
    867      case "::backdrop":
    868        return !isInherited && node.matches(":modal, :popover-open");
    869      case "::cue":
    870        return !isInherited && node.nodeName == "VIDEO";
    871      case "::file-selector-button":
    872        return !isInherited && node.nodeName == "INPUT" && node.type == "file";
    873      case "::details-content": {
    874        const isDetailsNode = node.nodeName == "DETAILS";
    875        if (!isDetailsNode) {
    876          return false;
    877        }
    878 
    879        if (!isInherited) {
    880          return true;
    881        }
    882 
    883        // If we're getting rules on a parent element, we need to check if the selected
    884        // element is inside the ::details-content of node
    885        // We traverse the flattened parent tree until we find the <slot> that implements
    886        // the pseudo element, as it's easier to handle edge cases like nested <details>,
    887        // multiple <summary>, etc …
    888        let traversedNode = this.selectedElement;
    889        while (traversedNode) {
    890          if (
    891            // if we found the <slot> implementing the pseudo element
    892            traversedNode.implementedPseudoElement === "::details-content" &&
    893            // and its parent <details> element is the element we're evaluating
    894            traversedNode.flattenedTreeParentNode === node
    895          ) {
    896            // then include the ::details-content rules from that element
    897            return true;
    898          }
    899          // otherwise keep looking up the tree
    900          traversedNode = traversedNode.flattenedTreeParentNode;
    901        }
    902 
    903        return false;
    904      }
    905      case "::placeholder":
    906      case "::-moz-placeholder":
    907        return !isInherited && this.#nodeIsTextfieldLike(node);
    908      case "::-moz-meter-bar":
    909        return !isInherited && node.nodeName == "METER";
    910      case "::-moz-progress-bar":
    911        return !isInherited && node.nodeName == "PROGRESS";
    912      case "::-moz-color-swatch":
    913        return !isInherited && node.nodeName == "INPUT" && node.type == "color";
    914      case "::-moz-range-progress":
    915      case "::-moz-range-thumb":
    916      case "::-moz-range-track":
    917      case "::slider-fill":
    918      case "::slider-thumb":
    919      case "::slider-track":
    920        return !isInherited && node.nodeName == "INPUT" && node.type == "range";
    921      case "::view-transition":
    922      case "::view-transition-group":
    923      case "::view-transition-image-pair":
    924      case "::view-transition-old":
    925      case "::view-transition-new":
    926        // FIXME: Bug 1909173. Need to handle view transitions peudo-elements
    927        // for DevTools. For now we skip them.
    928        return false;
    929      default:
    930        console.error("Unhandled pseudo-element " + pseudo);
    931        return false;
    932    }
    933  }
    934 
    935  /**
    936   * Helper function for #getAllElementRules, returns the rules from a given
    937   * element. See getApplied for documentation on parameters.
    938   *
    939   * @param {DOMNode} node
    940   * @param {string} pseudo
    941   * @param {NodeActor} inherited
    942   * @param {GetAppliedFilterOption} filter
    943   *
    944   * @returns Array
    945   */
    946  #getElementRules(node, pseudo, inherited, filter) {
    947    if (!Element.isInstance(node)) {
    948      return [];
    949    }
    950 
    951    // we don't need to retrieve inherited starting style rules
    952    const includeStartingStyleRules = !inherited;
    953    const domRules = InspectorUtils.getMatchingCSSRules(
    954      node,
    955      pseudo,
    956      CssLogic.hasVisitedState(node),
    957      includeStartingStyleRules
    958    );
    959 
    960    if (!domRules) {
    961      return [];
    962    }
    963 
    964    const rules = [];
    965 
    966    const doc = this.inspector.targetActor.window.document;
    967 
    968    // getMatchingCSSRules returns ordered from least-specific to
    969    // most-specific.
    970    for (let i = domRules.length - 1; i >= 0; i--) {
    971      const domRule = domRules[i];
    972      const isSystem =
    973        domRule.parentStyleSheet &&
    974        SharedCssLogic.isAgentStylesheet(domRule.parentStyleSheet);
    975 
    976      // For now, when dealing with InspectorDeclaration, we only care about presentational
    977      // hints style (e.g. <img height=100>).
    978      if (
    979        domRule.declarationOrigin &&
    980        domRule.declarationOrigin !== "pres-hints"
    981      ) {
    982        continue;
    983      }
    984 
    985      if (isSystem && filter != SharedCssLogic.FILTER.UA) {
    986        continue;
    987      }
    988 
    989      if (inherited) {
    990        // Don't include inherited rules if none of its properties
    991        // are inheritable.
    992        let hasInherited = false;
    993        // This can be on a hot path, so let's use a simple for rule instead of turning
    994        // domRule.style into an Array to use some on it.
    995        for (let j = 0, len = domRule.style.length; j < len; j++) {
    996          if (InspectorUtils.isInheritedProperty(doc, domRule.style[j])) {
    997            hasInherited = true;
    998            break;
    999          }
   1000        }
   1001 
   1002        if (!hasInherited) {
   1003          continue;
   1004        }
   1005      }
   1006 
   1007      const ruleActor = this.styleRef(domRule, pseudo);
   1008 
   1009      rules.push(
   1010        this.#getRuleItem(ruleActor, node, {
   1011          inherited,
   1012          isSystem,
   1013          pseudoElement: pseudo,
   1014        })
   1015      );
   1016    }
   1017    return rules;
   1018  }
   1019 
   1020  /**
   1021   * Given a node and a CSS rule, walk up the DOM looking for a matching element rule.
   1022   *
   1023   * @param {NodeActor} nodeActor the node
   1024   * @param {CSSStyleRule} matchingRule the rule to find the entry for
   1025   * @return {object | null} An entry as returned by #getAllElementRules, or null if no entry
   1026   *                       matching the passed rule was find
   1027   */
   1028  findEntryMatchingRule(nodeActor, matchingRule) {
   1029    let currentNodeActor = nodeActor;
   1030    while (
   1031      currentNodeActor &&
   1032      currentNodeActor.rawNode.nodeType != Node.DOCUMENT_NODE
   1033    ) {
   1034      for (const entry of this.#getAllElementRules(currentNodeActor, {
   1035        isInherited: nodeActor !== currentNodeActor,
   1036      })) {
   1037        if (entry.rule.rawRule === matchingRule) {
   1038          return entry;
   1039        }
   1040      }
   1041 
   1042      currentNodeActor = this.walker.parentNode(currentNodeActor);
   1043    }
   1044 
   1045    // If we reached the document node without finding the rule, return null
   1046    return null;
   1047  }
   1048 
   1049  /**
   1050   * Helper function for getApplied that fetches a set of style properties that
   1051   * apply to the given node and associated rules
   1052   *
   1053   * @param {NodeActor} node
   1054   * @param {Array} entries
   1055   *   List of appliedstyle objects that lists the rules that apply to the
   1056   *   node. If adding a new rule to the stylesheet, only the new rule entry
   1057   *   is provided and only the style properties that apply to the new
   1058   *   rule is fetched.
   1059   * @param {GetAppliedOptions} options
   1060   * @returns Array of rule entries that applies to the given node and its associated rules.
   1061   */
   1062  getAppliedProps(node, entries, options) {
   1063    if (options.inherited) {
   1064      let parent = this.walker.parentNode(node);
   1065      while (parent && parent.rawNode.nodeType != Node.DOCUMENT_NODE) {
   1066        entries = entries.concat(
   1067          this.#getAllElementRules(parent, {
   1068            isInherited: true,
   1069            skipPseudo: options.skipPseudo,
   1070            filter: options.filter,
   1071          })
   1072        );
   1073        parent = this.walker.parentNode(parent);
   1074      }
   1075    }
   1076 
   1077    if (options.matchedSelectors) {
   1078      for (const entry of entries) {
   1079        if (entry.rule.type === ELEMENT_STYLE) {
   1080          continue;
   1081        }
   1082        entry.matchedSelectorIndexes = [];
   1083 
   1084        const domRule = entry.rule.rawRule;
   1085        const element = entry.inherited
   1086          ? entry.inherited.rawNode
   1087          : node.rawNode;
   1088 
   1089        const pseudos = [];
   1090        const { bindingElement, pseudo } =
   1091          CssLogic.getBindingElementAndPseudo(element);
   1092 
   1093        // if we couldn't find a binding element, we can't call domRule.selectorMatchesElement,
   1094        // so bail out
   1095        if (!bindingElement) {
   1096          continue;
   1097        }
   1098 
   1099        if (pseudo) {
   1100          pseudos.push(pseudo);
   1101        } else if (entry.rule.pseudoElements.size) {
   1102          // if `node` is not a pseudo element but the rule applies to some pseudo elements,
   1103          // we need to pass those to CSSStyleRule#selectorMatchesElement
   1104          pseudos.push(...entry.rule.pseudoElements);
   1105        } else {
   1106          // If the rule doesn't apply to any pseudo, set a null item so we'll still do
   1107          // the proper check below
   1108          pseudos.push(null);
   1109        }
   1110 
   1111        const relevantLinkVisited = CssLogic.hasVisitedState(bindingElement);
   1112        const len = domRule.selectorCount;
   1113        for (let i = 0; i < len; i++) {
   1114          for (const pseudoElementName of pseudos) {
   1115            if (
   1116              domRule.selectorMatchesElement(
   1117                i,
   1118                bindingElement,
   1119                pseudoElementName,
   1120                relevantLinkVisited
   1121              )
   1122            ) {
   1123              entry.matchedSelectorIndexes.push(i);
   1124              // if we matched the selector for one pseudo, no need to check the other ones
   1125              break;
   1126            }
   1127          }
   1128        }
   1129      }
   1130    }
   1131 
   1132    const computedStyle = this.cssLogic.computedStyle;
   1133    if (computedStyle) {
   1134      // Add all the keyframes rule associated with the element
   1135      let animationNames = computedStyle.animationName.split(",");
   1136      animationNames = animationNames.map(name => name.trim());
   1137 
   1138      if (animationNames) {
   1139        // Traverse through all the available keyframes rule and add
   1140        // the keyframes rule that matches the computed animation name
   1141        for (const keyframesRule of this.cssLogic.keyframesRules) {
   1142          if (!animationNames.includes(keyframesRule.name)) {
   1143            continue;
   1144          }
   1145 
   1146          for (const rule of keyframesRule.cssRules) {
   1147            entries.push({
   1148              rule: this.styleRef(rule),
   1149              keyframes: this.styleRef(keyframesRule),
   1150            });
   1151          }
   1152        }
   1153      }
   1154 
   1155      // Add all the @position-try associated with the element
   1156      const positionTryIdents = new Set();
   1157      for (const part of computedStyle.positionTryFallbacks.split(",")) {
   1158        const name = part.trim();
   1159        if (name.startsWith("--")) {
   1160          positionTryIdents.add(name);
   1161        }
   1162      }
   1163 
   1164      for (const positionTryRule of this.cssLogic.positionTryRules) {
   1165        if (!positionTryIdents.has(positionTryRule.name)) {
   1166          continue;
   1167        }
   1168 
   1169        entries.push({
   1170          rule: this.styleRef(positionTryRule),
   1171        });
   1172      }
   1173    }
   1174 
   1175    return entries;
   1176  }
   1177 
   1178  /**
   1179   * Get layout-related information about a node.
   1180   * This method returns an object with properties giving information about
   1181   * the node's margin, border, padding and content region sizes, as well
   1182   * as information about the type of box, its position, z-index, etc...
   1183   *
   1184   * @param {NodeActor} node
   1185   * @param {object} options The only available option is autoMargins.
   1186   * If set to true, the element's margins will receive an extra check to see
   1187   * whether they are set to "auto" (knowing that the computed-style in this
   1188   * case would return "0px").
   1189   * The returned object will contain an extra property (autoMargins) listing
   1190   * all margins that are set to auto, e.g. {top: "auto", left: "auto"}.
   1191   * @return {object}
   1192   */
   1193  getLayout(node, options) {
   1194    this.cssLogic.highlight(node.rawNode);
   1195 
   1196    const layout = {};
   1197 
   1198    // First, we update the first part of the box model view, with
   1199    // the size of the element.
   1200 
   1201    const clientRect = node.rawNode.getBoundingClientRect();
   1202    layout.width = parseFloat(clientRect.width.toPrecision(6));
   1203    layout.height = parseFloat(clientRect.height.toPrecision(6));
   1204 
   1205    // We compute and update the values of margins & co.
   1206    const style = CssLogic.getComputedStyle(node.rawNode);
   1207    for (const prop of [
   1208      "position",
   1209      "top",
   1210      "right",
   1211      "bottom",
   1212      "left",
   1213      "margin-top",
   1214      "margin-right",
   1215      "margin-bottom",
   1216      "margin-left",
   1217      "padding-top",
   1218      "padding-right",
   1219      "padding-bottom",
   1220      "padding-left",
   1221      "border-top-width",
   1222      "border-right-width",
   1223      "border-bottom-width",
   1224      "border-left-width",
   1225      "z-index",
   1226      "box-sizing",
   1227      "display",
   1228      "float",
   1229      "line-height",
   1230    ]) {
   1231      layout[prop] = style.getPropertyValue(prop);
   1232    }
   1233 
   1234    if (options.autoMargins) {
   1235      layout.autoMargins = this.processMargins(this.cssLogic);
   1236    }
   1237 
   1238    for (const i in this.map) {
   1239      const property = this.map[i].property;
   1240      this.map[i].value = parseFloat(style.getPropertyValue(property));
   1241    }
   1242 
   1243    return layout;
   1244  }
   1245 
   1246  /**
   1247   * Find 'auto' margin properties.
   1248   */
   1249  processMargins(cssLogic) {
   1250    const margins = {};
   1251 
   1252    for (const prop of ["top", "bottom", "left", "right"]) {
   1253      const info = cssLogic.getPropertyInfo("margin-" + prop);
   1254      const selectors = info.matchedSelectors;
   1255      if (selectors && !!selectors.length && selectors[0].value == "auto") {
   1256        margins[prop] = "auto";
   1257      }
   1258    }
   1259 
   1260    return margins;
   1261  }
   1262 
   1263  /**
   1264   * On page navigation, tidy up remaining objects.
   1265   */
   1266  onFrameUnload() {
   1267    this.styleSheetsByRootNode = new WeakMap();
   1268  }
   1269 
   1270  #onStylesheetUpdated = ({ resourceId, updateKind, updates = {} }) => {
   1271    if (updateKind != "style-applied") {
   1272      return;
   1273    }
   1274    const kind = updates.event.kind;
   1275    // Duplicate refMap content before looping as onStyleApplied may mutate it
   1276    for (const styleActor of [...this.refMap.values()]) {
   1277      // Ignore StyleRuleActor that don't have a parent stylesheet.
   1278      // i.e. actor whose type is ELEMENT_STYLE.
   1279      if (!styleActor._parentSheet) {
   1280        continue;
   1281      }
   1282      const resId = this.styleSheetsManager.getStyleSheetResourceId(
   1283        styleActor._parentSheet
   1284      );
   1285      if (resId === resourceId) {
   1286        styleActor.onStyleApplied(kind);
   1287      }
   1288    }
   1289    this.#styleApplied(kind);
   1290  };
   1291 
   1292  /**
   1293   * Helper function for adding a new rule and getting its applied style
   1294   * properties
   1295   *
   1296   * @param NodeActor node
   1297   * @param CSSStyleRule rule
   1298   * @returns Array containing its applied style properties
   1299   */
   1300  getNewAppliedProps(node, rule) {
   1301    const ruleActor = this.styleRef(rule);
   1302    return this.getAppliedProps(node, [{ rule: ruleActor }], {
   1303      matchedSelectors: true,
   1304    });
   1305  }
   1306 
   1307  /**
   1308   * Adds a new rule, and returns the new StyleRuleActor.
   1309   *
   1310   * @param {NodeActor} node
   1311   * @param {string} pseudoClasses The list of pseudo classes to append to the
   1312   *        new selector.
   1313   * @returns {StyleRuleActor} the new rule
   1314   */
   1315  async addNewRule(node, pseudoClasses) {
   1316    let sheet = null;
   1317    const doc = node.rawNode.ownerDocument;
   1318    const rootNode = node.rawNode.getRootNode();
   1319 
   1320    if (
   1321      this.styleSheetsByRootNode.has(rootNode) &&
   1322      this.styleSheetsByRootNode.get(rootNode).ownerNode?.isConnected
   1323    ) {
   1324      sheet = this.styleSheetsByRootNode.get(rootNode);
   1325    } else {
   1326      sheet = await this.styleSheetsManager.addStyleSheet(
   1327        doc,
   1328        node.rawNode.containingShadowRoot || doc.documentElement
   1329      );
   1330      this.styleSheetsByRootNode.set(rootNode, sheet);
   1331    }
   1332 
   1333    const cssRules = sheet.cssRules;
   1334 
   1335    // Get the binding element in case node is a pseudo element, so we can properly
   1336    // build the selector
   1337    const { bindingElement, pseudo } = CssLogic.getBindingElementAndPseudo(
   1338      node.rawNode
   1339    );
   1340    const classes = [...bindingElement.classList];
   1341 
   1342    let selector;
   1343    if (bindingElement.id) {
   1344      selector = "#" + CSS.escape(bindingElement.id);
   1345    } else if (classes.length) {
   1346      selector = "." + classes.map(c => CSS.escape(c)).join(".");
   1347    } else {
   1348      selector = bindingElement.localName;
   1349    }
   1350 
   1351    if (pseudo && pseudoClasses?.length) {
   1352      throw new Error(
   1353        `Can't set pseudo classes (${JSON.stringify(pseudoClasses)}) onto a pseudo element (${pseudo})`
   1354      );
   1355    }
   1356 
   1357    if (pseudo) {
   1358      selector += pseudo;
   1359    }
   1360    if (pseudoClasses && pseudoClasses.length) {
   1361      selector += pseudoClasses.join("");
   1362    }
   1363 
   1364    const index = sheet.insertRule(selector + " {}", cssRules.length);
   1365 
   1366    const resourceId = this.styleSheetsManager.getStyleSheetResourceId(sheet);
   1367    let authoredText = await this.styleSheetsManager.getText(resourceId);
   1368    authoredText += "\n" + selector + " {\n" + "}";
   1369    await this.styleSheetsManager.setStyleSheetText(resourceId, authoredText);
   1370 
   1371    const cssRule = sheet.cssRules.item(index);
   1372    const ruleActor = this.styleRef(cssRule, null, true);
   1373 
   1374    this.inspector.targetActor.emit("track-css-change", {
   1375      ...ruleActor.metadata,
   1376      type: "rule-add",
   1377      add: null,
   1378      remove: null,
   1379      selector,
   1380    });
   1381 
   1382    return { entries: this.getNewAppliedProps(node, cssRule) };
   1383  }
   1384 
   1385  /**
   1386   * Cause all StyleRuleActor instances of observed CSS rules to check whether the
   1387   * states of their declarations have changed.
   1388   *
   1389   * Observed rules are the latest rules returned by a call to PageStyleActor.getApplied()
   1390   *
   1391   * This is necessary because changes in one rule can cause the declarations in another
   1392   * to not be applicable (inactive CSS). The observers of those rules should be notified.
   1393   * Rules will fire a "rule-updated" event if any of their declarations changed state.
   1394   *
   1395   * Call this method whenever a CSS rule is mutated:
   1396   * - a CSS declaration is added/changed/disabled/removed
   1397   * - a selector is added/changed/removed
   1398   *
   1399   * @param {Array<StyleRuleActor>} rulesToForceRefresh: An array of rules that,
   1400   *        if observed, should be refreshed even if the state of their declaration
   1401   *        didn't change.
   1402   */
   1403  refreshObservedRules(rulesToForceRefresh) {
   1404    for (const rule of this.#observedRules) {
   1405      const force = rulesToForceRefresh && rulesToForceRefresh.includes(rule);
   1406      rule.maybeRefresh(force);
   1407    }
   1408  }
   1409 
   1410  /**
   1411   * Get an array of existing attribute values in a node document.
   1412   *
   1413   * @param {string} search: A string to filter attribute value on.
   1414   * @param {string} attributeType: The type of attribute we want to retrieve the values.
   1415   * @param {Element} node: The element we want to get possible attributes for. This will
   1416   *        be used to get the document where the search is happening.
   1417   * @returns {Array<string>} An array of strings
   1418   */
   1419  getAttributesInOwnerDocument(search, attributeType, node) {
   1420    if (!search) {
   1421      throw new Error("search is mandatory");
   1422    }
   1423 
   1424    // In a non-fission world, a node from an iframe shares the same `rootNode` as a node
   1425    // in the top-level document. So here we need to retrieve the document from the node
   1426    // in parameter in order to retrieve the right document.
   1427    // This may change once we have a dedicated walker for every target in a tab, as we'll
   1428    // be able to directly talk to the "right" walker actor.
   1429    const targetDocument = node.rawNode.ownerDocument;
   1430 
   1431    // We store the result in a Set which will contain the attribute value
   1432    const result = new Set();
   1433    const lcSearch = search.toLowerCase();
   1434    this.#collectAttributesFromDocumentDOM(
   1435      result,
   1436      lcSearch,
   1437      attributeType,
   1438      targetDocument,
   1439      node.rawNode
   1440    );
   1441    this.#collectAttributesFromDocumentStyleSheets(
   1442      result,
   1443      lcSearch,
   1444      attributeType,
   1445      targetDocument
   1446    );
   1447 
   1448    return Array.from(result).sort();
   1449  }
   1450 
   1451  /**
   1452   * Collect attribute values from the document DOM tree, matching the passed filter and
   1453   * type, to the result Set.
   1454   *
   1455   * @param {Set<string>} result: A Set to which the results will be added.
   1456   * @param {string} search: A string to filter attribute value on.
   1457   * @param {string} attributeType: The type of attribute we want to retrieve the values.
   1458   * @param {Document} targetDocument: The document the search occurs in.
   1459   * @param {Node} currentNode: The current element rawNode
   1460   */
   1461  #collectAttributesFromDocumentDOM(
   1462    result,
   1463    search,
   1464    attributeType,
   1465    targetDocument,
   1466    nodeRawNode
   1467  ) {
   1468    // In order to retrieve attributes from DOM elements in the document, we're going to
   1469    // do a query on the root node using attributes selector, to directly get the elements
   1470    // matching the attributes we're looking for.
   1471 
   1472    // For classes, we need something a bit different as the className we're looking
   1473    // for might not be the first in the attribute value, meaning we can't use the
   1474    // "attribute starts with X" selector.
   1475    const attributeSelectorPositionChar = attributeType === "class" ? "*" : "^";
   1476    const selector = `[${attributeType}${attributeSelectorPositionChar}=${search} i]`;
   1477 
   1478    const matchingElements = targetDocument.querySelectorAll(selector);
   1479 
   1480    for (const element of matchingElements) {
   1481      if (element === nodeRawNode) {
   1482        return;
   1483      }
   1484      // For class attribute, we need to add the elements of the classList that match
   1485      // the filter string.
   1486      if (attributeType === "class") {
   1487        for (const cls of element.classList) {
   1488          if (!result.has(cls) && cls.toLowerCase().startsWith(search)) {
   1489            result.add(cls);
   1490          }
   1491        }
   1492      } else {
   1493        const { value } = element.attributes[attributeType];
   1494        // For other attributes, we can directly use the attribute value.
   1495        result.add(value);
   1496      }
   1497    }
   1498  }
   1499 
   1500  /**
   1501   * Collect attribute values from the document stylesheets, matching the passed filter
   1502   * and type, to the result Set.
   1503   *
   1504   * @param {Set<string>} result: A Set to which the results will be added.
   1505   * @param {string} search: A string to filter attribute value on.
   1506   * @param {string} attributeType: The type of attribute we want to retrieve the values.
   1507   *                       It only supports "class" and "id" at the moment.
   1508   * @param {Document} targetDocument: The document the search occurs in.
   1509   */
   1510  #collectAttributesFromDocumentStyleSheets(
   1511    result,
   1512    search,
   1513    attributeType,
   1514    targetDocument
   1515  ) {
   1516    if (attributeType !== "class" && attributeType !== "id") {
   1517      return;
   1518    }
   1519 
   1520    // We loop through all the stylesheets and their rules, recursively so we can go through
   1521    // nested rules, and then use the lexer to only get the attributes we're looking for.
   1522    const traverseRules = ruleList => {
   1523      for (const rule of ruleList) {
   1524        this.#collectAttributesFromRule(result, rule, search, attributeType);
   1525        if (rule.cssRules) {
   1526          traverseRules(rule.cssRules);
   1527        }
   1528      }
   1529    };
   1530    for (const styleSheet of targetDocument.styleSheets) {
   1531      traverseRules(styleSheet.rules);
   1532    }
   1533  }
   1534 
   1535  /**
   1536   * Collect attribute values from the rule, matching the passed filter and type, to the
   1537   * result Set.
   1538   *
   1539   * @param {Set<string>} result: A Set to which the results will be added.
   1540   * @param {Rule} rule: The rule the search occurs in.
   1541   * @param {string} search: A string to filter attribute value on.
   1542   * @param {string} attributeType: The type of attribute we want to retrieve the values.
   1543   *                       It only supports "class" and "id" at the moment.
   1544   */
   1545  #collectAttributesFromRule(result, rule, search, attributeType) {
   1546    const shouldRetrieveClasses = attributeType === "class";
   1547    const shouldRetrieveIds = attributeType === "id";
   1548 
   1549    const { selectorText } = rule;
   1550    // If there's no selectorText, or if the selectorText does not include the
   1551    // filter, we can bail out.
   1552    if (!selectorText || !selectorText.toLowerCase().includes(search)) {
   1553      return;
   1554    }
   1555 
   1556    // Check if we should parse the selectorText (do we need to check for class/id and
   1557    // if so, does the selector contains class/id related chars).
   1558    const parseForClasses =
   1559      shouldRetrieveClasses &&
   1560      selectorText.toLowerCase().includes(`.${search}`);
   1561    const parseForIds =
   1562      shouldRetrieveIds && selectorText.toLowerCase().includes(`#${search}`);
   1563 
   1564    if (!parseForClasses && !parseForIds) {
   1565      return;
   1566    }
   1567 
   1568    const lexer = new InspectorCSSParser(selectorText);
   1569    let token;
   1570    while ((token = lexer.nextToken())) {
   1571      if (
   1572        token.tokenType === "Delim" &&
   1573        shouldRetrieveClasses &&
   1574        token.text === "."
   1575      ) {
   1576        token = lexer.nextToken();
   1577        if (
   1578          token.tokenType === "Ident" &&
   1579          token.text.toLowerCase().startsWith(search)
   1580        ) {
   1581          result.add(token.text);
   1582        }
   1583      }
   1584      if (token.tokenType === "IDHash" && shouldRetrieveIds) {
   1585        const idWithoutHash = token.value;
   1586        if (idWithoutHash.startsWith(search)) {
   1587          result.add(idWithoutHash);
   1588        }
   1589      }
   1590    }
   1591  }
   1592 }
   1593 exports.PageStyleActor = PageStyleActor;