tor-browser

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

css-logic.js (51248B)


      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 /*
      6 * About the objects defined in this file:
      7 * - CssLogic contains style information about a view context. It provides
      8 *   access to 2 sets of objects: Css[Sheet|Rule|Selector] provide access to
      9 *   information that does not change when the selected element changes while
     10 *   Css[Property|Selector]Info provide information that is dependent on the
     11 *   selected element.
     12 *   Its key methods are highlight(), getPropertyInfo() and forEachSheet(), etc
     13 *
     14 * - CssSheet provides a more useful API to a DOM CSSSheet for our purposes,
     15 *   including shortSource and href.
     16 * - CssRule a more useful API to a DOM CSSRule including access to the group
     17 *   of CssSelectors that the rule provides properties for
     18 * - CssSelector A single selector - i.e. not a selector group. In other words
     19 *   a CssSelector does not contain ','. This terminology is different from the
     20 *   standard DOM API, but more inline with the definition in the spec.
     21 *
     22 * - CssPropertyInfo contains style information for a single property for the
     23 *   highlighted element.
     24 * - CssSelectorInfo is a wrapper around CssSelector, which adds sorting with
     25 *   reference to the selected element.
     26 */
     27 
     28 "use strict";
     29 
     30 const nodeConstants = require("resource://devtools/shared/dom-node-constants.js");
     31 const {
     32  getBindingElementAndPseudo,
     33  getMatchingCSSRules,
     34  hasVisitedState,
     35  isAgentStylesheet,
     36  isAuthorStylesheet,
     37  isUserStylesheet,
     38  shortSource,
     39  ELEMENT_BACKED_PSEUDO_ELEMENTS,
     40  FILTER,
     41  STATUS,
     42 } = require("resource://devtools/shared/inspector/css-logic.js");
     43 
     44 const COMPAREMODE = {
     45  BOOLEAN: "bool",
     46  INTEGER: "int",
     47 };
     48 
     49 class CssLogic {
     50  /**
     51   * If the element has an id, return '#id'. Otherwise return 'tagname[n]' where
     52   * n is the index of this element in its siblings.
     53   * <p>A technically more 'correct' output from the no-id case might be:
     54   * 'tagname:nth-of-type(n)' however this is unlikely to be more understood
     55   * and it is longer.
     56   *
     57   * @param {Element} element the element for which you want the short name.
     58   * @return {string} the string to be displayed for element.
     59   */
     60  static getShortName(element) {
     61    if (!element) {
     62      return "null";
     63    }
     64    if (element.id) {
     65      return "#" + element.id;
     66    }
     67    let priorSiblings = 0;
     68    let temp = element;
     69    while ((temp = temp.previousElementSibling)) {
     70      priorSiblings++;
     71    }
     72    return element.tagName + "[" + priorSiblings + "]";
     73  }
     74 
     75  /**
     76   * Get a string list of selectors for a given DOMRule.
     77   *
     78   * @param {DOMRule} domRule
     79   *        The DOMRule to parse.
     80   * @param {boolean} desugared
     81   *        Set to true to get the desugared selector (see https://drafts.csswg.org/css-nesting-1/#nest-selector)
     82   * @return {Array}
     83   *         An array of string selectors.
     84   */
     85  static getSelectors(domRule, desugared = false) {
     86    if (ChromeUtils.getClassName(domRule) !== "CSSStyleRule") {
     87      // Return empty array since CSSRule#selectorCount assumes only STYLE_RULE type.
     88      return [];
     89    }
     90 
     91    const selectors = [];
     92 
     93    const len = domRule.selectorCount;
     94    for (let i = 0; i < len; i++) {
     95      selectors.push(domRule.selectorTextAt(i, desugared));
     96    }
     97    return selectors;
     98  }
     99 
    100  /**
    101   * Given a node, check to see if it is a ::before or ::after element.
    102   * If so, return the node that is accessible from within the document
    103   * (the parent of the anonymous node), along with which pseudo element
    104   * it was.  Otherwise, return the node itself.
    105   *
    106   * @returns {object}
    107   *            - {DOMNode} node The non-anonymous node
    108   *            - {string} pseudo One of ':marker', ':before', ':after', or null.
    109   */
    110  static getBindingElementAndPseudo = getBindingElementAndPseudo;
    111 
    112  /**
    113   * Get the computed style on a node.  Automatically handles reading
    114   * computed styles on a ::before/::after element by reading on the
    115   * parent node with the proper pseudo argument.
    116   *
    117   * @param {Node}
    118   * @returns {CSSStyleDeclaration}
    119   */
    120  static getComputedStyle(node) {
    121    if (
    122      !node ||
    123      Cu.isDeadWrapper(node) ||
    124      node.nodeType !== nodeConstants.ELEMENT_NODE ||
    125      !node.ownerGlobal
    126    ) {
    127      return null;
    128    }
    129 
    130    const { bindingElement, pseudo } =
    131      CssLogic.getBindingElementAndPseudo(node);
    132 
    133    // For reasons that still escape us, pseudo-elements can sometimes be "unattached" (i.e.
    134    // not have a parentNode defined). This seems to happen when a page is reloaded while
    135    // the inspector is open. Bailing out here ensures that the inspector does not fail at
    136    // presenting DOM nodes and CSS styles when this happens. This is a temporary measure.
    137    // See bug 1506792.
    138    if (!bindingElement) {
    139      return null;
    140    }
    141 
    142    return node.ownerGlobal.getComputedStyle(bindingElement, pseudo);
    143  }
    144 
    145  /**
    146   * Get a source for a stylesheet, taking into account embedded stylesheets
    147   * for which we need to use document.defaultView.location.href rather than
    148   * sheet.href
    149   *
    150   * @param {CSSStyleSheet} sheet the DOM object for the style sheet.
    151   * @return {string} the address of the stylesheet.
    152   */
    153  static href(sheet) {
    154    return sheet.href || sheet.associatedDocument.location;
    155  }
    156 
    157  /**
    158   * Returns true if the given node has visited state.
    159   */
    160  static hasVisitedState = hasVisitedState;
    161 
    162  // Used for tracking matched CssSelector objects.
    163  matchId = 0;
    164 
    165  // Used for tracking unique CssSheet/CssRule/CssSelector objects, in a run of
    166  // processMatchedSelectors().
    167  passId = 0;
    168 
    169  // Both setup by highlight().
    170  viewedElement = null;
    171  viewedDocument = null;
    172 
    173  #propertyInfos = {};
    174 
    175  // The cache of the known sheets.
    176  #sheets = null;
    177 
    178  // Have the sheets been cached?
    179  #sheetsCached = false;
    180 
    181  #sheetIndex = 0;
    182 
    183  // The total number of rules, in all stylesheets, after filtering.
    184  #ruleCount = 0;
    185 
    186  // The computed styles for the viewedElement.
    187  #computedStyle = null;
    188 
    189  // Source filter. Only display properties coming from the given source
    190  #sourceFilter = FILTER.USER;
    191 
    192  #matchedRules = null;
    193  #matchedSelectors = null;
    194 
    195  // Cached keyframes rules in all stylesheets
    196  #keyframesRules = null;
    197 
    198  // Cached @position-try rules in all stylesheets
    199  #positionTryRules = null;
    200 
    201  /**
    202   * Reset various properties
    203   */
    204  reset() {
    205    this.#propertyInfos = {};
    206    this.#ruleCount = 0;
    207    this.#sheetIndex = 0;
    208    this.#sheets = {};
    209    this.#sheetsCached = false;
    210    this.#matchedRules = null;
    211    this.#matchedSelectors = null;
    212    this.#keyframesRules = [];
    213    this.#positionTryRules = [];
    214  }
    215 
    216  /**
    217   * Focus on a new element - remove the style caches.
    218   *
    219   * @param {Element} aViewedElement the element the user has highlighted
    220   * in the Inspector.
    221   */
    222  highlight(viewedElement) {
    223    if (!viewedElement) {
    224      this.viewedElement = null;
    225      this.viewedDocument = null;
    226      this.#computedStyle = null;
    227      this.reset();
    228      return;
    229    }
    230 
    231    if (viewedElement === this.viewedElement) {
    232      return;
    233    }
    234 
    235    this.viewedElement = viewedElement;
    236 
    237    const doc = this.viewedElement.ownerDocument;
    238    if (doc != this.viewedDocument) {
    239      // New document: clear/rebuild the cache.
    240      this.viewedDocument = doc;
    241 
    242      // Hunt down top level stylesheets, and cache them.
    243      this.#cacheSheets();
    244    } else {
    245      // Clear cached data in the CssPropertyInfo objects.
    246      this.#propertyInfos = {};
    247    }
    248 
    249    this.#matchedRules = null;
    250    this.#matchedSelectors = null;
    251    this.#computedStyle = CssLogic.getComputedStyle(this.viewedElement);
    252  }
    253 
    254  /**
    255   * Get the values of all the computed CSS properties for the highlighted
    256   * element.
    257   *
    258   * @returns {object} The computed CSS properties for a selected element
    259   */
    260  get computedStyle() {
    261    return this.#computedStyle;
    262  }
    263 
    264  /**
    265   * Get the source filter.
    266   *
    267   * @returns {string} The source filter being used.
    268   */
    269  get sourceFilter() {
    270    return this.#sourceFilter;
    271  }
    272 
    273  /**
    274   * Source filter. Only display properties coming from the given source (web
    275   * address). Note that in order to avoid information overload we DO NOT show
    276   * unmatched system rules.
    277   *
    278   * @see FILTER.*
    279   */
    280  set sourceFilter(value) {
    281    const oldValue = this.#sourceFilter;
    282    this.#sourceFilter = value;
    283 
    284    let ruleCount = 0;
    285 
    286    // Update the CssSheet objects.
    287    this.forEachSheet(function (sheet) {
    288      if (sheet.authorSheet && sheet.sheetAllowed) {
    289        ruleCount += sheet.ruleCount;
    290      }
    291    }, this);
    292 
    293    this.#ruleCount = ruleCount;
    294 
    295    // Full update is needed because the this.processMatchedSelectors() method
    296    // skips UA stylesheets if the filter does not allow such sheets.
    297    const needFullUpdate = oldValue == FILTER.UA || value == FILTER.UA;
    298 
    299    if (needFullUpdate) {
    300      this.#matchedRules = null;
    301      this.#matchedSelectors = null;
    302      this.#propertyInfos = {};
    303    } else {
    304      // Update the CssPropertyInfo objects.
    305      for (const property in this.#propertyInfos) {
    306        this.#propertyInfos[property].needRefilter = true;
    307      }
    308    }
    309  }
    310 
    311  /**
    312   * Return a CssPropertyInfo data structure for the currently viewed element
    313   * and the specified CSS property. If there is no currently viewed element we
    314   * return an empty object.
    315   *
    316   * @param {string} property The CSS property to look for.
    317   * @return {CssPropertyInfo} a CssPropertyInfo structure for the given
    318   * property.
    319   */
    320  getPropertyInfo(property) {
    321    if (!this.viewedElement) {
    322      return {};
    323    }
    324 
    325    let info = this.#propertyInfos[property];
    326    if (!info) {
    327      info = new CssPropertyInfo(this, property);
    328      this.#propertyInfos[property] = info;
    329    }
    330 
    331    return info;
    332  }
    333 
    334  /**
    335   * Cache all the stylesheets in the inspected document
    336   *
    337   * @private
    338   */
    339  #cacheSheets() {
    340    this.passId++;
    341    this.reset();
    342 
    343    // styleSheets isn't an array, but forEach can work on it anyway
    344    const styleSheets = InspectorUtils.getAllStyleSheets(
    345      this.viewedDocument,
    346      true
    347    );
    348    Array.prototype.forEach.call(styleSheets, this.#cacheSheet, this);
    349 
    350    this.#sheetsCached = true;
    351  }
    352 
    353  /**
    354   * Cache a stylesheet if it falls within the requirements: if it's enabled,
    355   * and if the @media is allowed. This method also walks through the stylesheet
    356   * cssRules to find @imported rules, to cache the stylesheets of those rules
    357   * as well. In addition, @keyframes and @position-try rules in the stylesheet are cached.
    358   *
    359   * @private
    360   * @param {CSSStyleSheet} domSheet the CSSStyleSheet object to cache.
    361   */
    362  #cacheSheet(domSheet) {
    363    if (domSheet.disabled) {
    364      return;
    365    }
    366 
    367    // Only work with stylesheets that have their media allowed.
    368    if (!this.mediaMatches(domSheet)) {
    369      return;
    370    }
    371 
    372    // Cache the sheet.
    373    const cssSheet = this.getSheet(domSheet, this.#sheetIndex++);
    374    if (cssSheet.passId != this.passId) {
    375      cssSheet.passId = this.passId;
    376 
    377      // Find import, keyframes and position-try rules. We loop through all the stylesheet
    378      // recursively, so we can go through nested rules.
    379      const traverseRules = ruleList => {
    380        for (const aDomRule of ruleList) {
    381          const ruleClassName = ChromeUtils.getClassName(aDomRule);
    382          if (
    383            ruleClassName === "CSSImportRule" &&
    384            aDomRule.styleSheet &&
    385            this.mediaMatches(aDomRule)
    386          ) {
    387            this.#cacheSheet(aDomRule.styleSheet);
    388          } else if (ruleClassName === "CSSKeyframesRule") {
    389            this.#keyframesRules.push(aDomRule);
    390          } else if (ruleClassName === "CSSPositionTryRule") {
    391            this.#positionTryRules.push(aDomRule);
    392          }
    393 
    394          if (aDomRule.cssRules) {
    395            traverseRules(aDomRule.cssRules);
    396          }
    397        }
    398      };
    399 
    400      traverseRules(cssSheet.getCssRules());
    401    }
    402  }
    403 
    404  /**
    405   * Retrieve the list of stylesheets in the document.
    406   *
    407   * @return {Array} the list of stylesheets in the document.
    408   */
    409  get sheets() {
    410    if (!this.#sheetsCached) {
    411      this.#cacheSheets();
    412    }
    413 
    414    const sheets = [];
    415    this.forEachSheet(function (sheet) {
    416      if (sheet.authorSheet) {
    417        sheets.push(sheet);
    418      }
    419    }, this);
    420 
    421    return sheets;
    422  }
    423 
    424  /**
    425   * Retrieve the list of keyframes rules in the document.
    426   *
    427   * @ return {array} the list of keyframes rules in the document.
    428   */
    429  get keyframesRules() {
    430    if (!this.#sheetsCached) {
    431      this.#cacheSheets();
    432    }
    433    return this.#keyframesRules;
    434  }
    435 
    436  /**
    437   * Retrieve the list of @position-try rules in the document.
    438   *
    439   * @returns {CSSPositionTryRule[]} the list of @position-try rules in the document.
    440   */
    441  get positionTryRules() {
    442    if (!this.#sheetsCached) {
    443      this.#cacheSheets();
    444    }
    445    return this.#positionTryRules;
    446  }
    447 
    448  /**
    449   * Retrieve a CssSheet object for a given a CSSStyleSheet object. If the
    450   * stylesheet is already cached, you get the existing CssSheet object,
    451   * otherwise the new CSSStyleSheet object is cached.
    452   *
    453   * @param {CSSStyleSheet} domSheet the CSSStyleSheet object you want.
    454   * @param {number} index the index, within the document, of the stylesheet.
    455   *
    456   * @return {CssSheet} the CssSheet object for the given CSSStyleSheet object.
    457   */
    458  getSheet(domSheet, index) {
    459    let cacheId = "";
    460 
    461    if (domSheet.href) {
    462      cacheId = domSheet.href;
    463    } else if (domSheet.associatedDocument) {
    464      cacheId = domSheet.associatedDocument.location;
    465    }
    466 
    467    let sheet = null;
    468    let sheetFound = false;
    469 
    470    if (cacheId in this.#sheets) {
    471      for (sheet of this.#sheets[cacheId]) {
    472        if (sheet.domSheet === domSheet) {
    473          if (index != -1) {
    474            sheet.index = index;
    475          }
    476          sheetFound = true;
    477          break;
    478        }
    479      }
    480    }
    481 
    482    if (!sheetFound) {
    483      if (!(cacheId in this.#sheets)) {
    484        this.#sheets[cacheId] = [];
    485      }
    486 
    487      sheet = new CssSheet(this, domSheet, index);
    488      if (sheet.sheetAllowed && sheet.authorSheet) {
    489        this.#ruleCount += sheet.ruleCount;
    490      }
    491 
    492      this.#sheets[cacheId].push(sheet);
    493    }
    494 
    495    return sheet;
    496  }
    497 
    498  /**
    499   * Process each cached stylesheet in the document using your callback.
    500   *
    501   * @param {function} callback the function you want executed for each of the
    502   * CssSheet objects cached.
    503   * @param {object} scope the scope you want for the callback function. scope
    504   * will be the this object when callback executes.
    505   */
    506  forEachSheet(callback, scope) {
    507    for (const cacheId in this.#sheets) {
    508      const sheets = this.#sheets[cacheId];
    509      for (let i = 0; i < sheets.length; i++) {
    510        // We take this as an opportunity to clean dead sheets
    511        try {
    512          const sheet = sheets[i];
    513          // If accessing domSheet raises an exception, then the style
    514          // sheet is a dead object.
    515          sheet.domSheet;
    516          callback.call(scope, sheet, i, sheets);
    517        } catch (e) {
    518          sheets.splice(i, 1);
    519          i--;
    520        }
    521      }
    522    }
    523  }
    524 
    525  /**
    526 
    527  /**
    528   * Get the number CSSRule objects in the document, counted from all of
    529   * the stylesheets. System sheets are excluded. If a filter is active, this
    530   * tells only the number of CSSRule objects inside the selected
    531   * CSSStyleSheet.
    532   *
    533   * WARNING: This only provides an estimate of the rule count, and the results
    534   * could change at a later date. Todo remove this
    535   *
    536   * @return {number} the number of CSSRule (all rules).
    537   */
    538  get ruleCount() {
    539    if (!this.#sheetsCached) {
    540      this.#cacheSheets();
    541    }
    542 
    543    return this.#ruleCount;
    544  }
    545 
    546  /**
    547   * Process the CssSelector objects that match the highlighted element and its
    548   * parent elements. scope.callback() is executed for each CssSelector
    549   * object, being passed the CssSelector object and the match status.
    550   *
    551   * This method also includes all of the element.style properties, for each
    552   * highlighted element parent and for the highlighted element itself.
    553   *
    554   * Note that the matched selectors are cached, such that next time your
    555   * callback is invoked for the cached list of CssSelector objects.
    556   *
    557   * @param {function} callback the function you want to execute for each of
    558   * the matched selectors.
    559   * @param {object} scope the scope you want for the callback function. scope
    560   * will be the this object when callback executes.
    561   */
    562  processMatchedSelectors(callback, scope) {
    563    if (this.#matchedSelectors) {
    564      if (callback) {
    565        this.passId++;
    566        this.#matchedSelectors.forEach(function (value) {
    567          callback.call(scope, value[0], value[1]);
    568          value[0].cssRule.passId = this.passId;
    569        }, this);
    570      }
    571      return;
    572    }
    573 
    574    if (!this.#matchedRules) {
    575      this.#buildMatchedRules();
    576    }
    577 
    578    this.#matchedSelectors = [];
    579    this.passId++;
    580 
    581    for (const matchedRule of this.#matchedRules) {
    582      const [rule, status, distance] = matchedRule;
    583 
    584      rule.selectors.forEach(function (selector) {
    585        if (
    586          selector.matchId !== this.matchId &&
    587          (rule.domRule.declarationOrigin === "style-attribute" ||
    588            rule.domRule.declarationOrigin === "pres-hints" ||
    589            this.selectorMatchesElement(rule.domRule, selector.selectorIndex))
    590        ) {
    591          selector.matchId = this.matchId;
    592          this.#matchedSelectors.push([selector, status, distance]);
    593          if (callback) {
    594            callback.call(scope, selector, status, distance);
    595          }
    596        }
    597      }, this);
    598 
    599      rule.passId = this.passId;
    600    }
    601  }
    602 
    603  /**
    604   * Check if the given selector matches the highlighted element or any of its
    605   * parents.
    606   *
    607   * @private
    608   * @param {DOMRule} domRule
    609   *        The DOM Rule containing the selector.
    610   * @param {number} idx
    611   *        The index of the selector within the DOMRule.
    612   * @return {boolean}
    613   *         true if the given selector matches the highlighted element or any
    614   *         of its parents, otherwise false is returned.
    615   */
    616  selectorMatchesElement(domRule, idx) {
    617    let element = this.viewedElement;
    618    do {
    619      const { bindingElement, pseudo } =
    620        CssLogic.getBindingElementAndPseudo(element);
    621      if (domRule.selectorMatchesElement(idx, bindingElement, pseudo)) {
    622        return true;
    623      }
    624 
    625      for (const pseudoElement of ELEMENT_BACKED_PSEUDO_ELEMENTS) {
    626        if (domRule.selectorMatchesElement(idx, element, pseudoElement)) {
    627          return true;
    628        }
    629      }
    630    } while (
    631      // Loop on flattenedTreeParentNode instead of parentNode to reach the
    632      // shadow host from the shadow dom.
    633      (element = element.flattenedTreeParentNode) &&
    634      element.nodeType === nodeConstants.ELEMENT_NODE
    635    );
    636 
    637    return false;
    638  }
    639 
    640  /**
    641   * Check if the highlighted element or its parents have matched selectors.
    642   *
    643   * @param {Array<string>} properties: The list of properties you want to check if they
    644   * have matched selectors or not. For CSS variables, this will check if the variable
    645   * is set OR used in a matching rule.
    646   * @return {Set<string>} A Set containing the properties that do have matched selectors.
    647   */
    648  hasMatchedSelectors(properties) {
    649    if (!this.#matchedRules) {
    650      this.#buildMatchedRules();
    651    }
    652 
    653    const result = new Set();
    654 
    655    for (const [rule, status] of this.#matchedRules) {
    656      // Getting the rule cssText can be costly, so cache it
    657      let cssText;
    658      const getCssText = () => {
    659        if (cssText === undefined) {
    660          cssText = rule.domRule.cssText;
    661        }
    662        return cssText;
    663      };
    664 
    665      // Loop through properties in reverse as we're removing items from it and we don't
    666      // want to mess with the iteration.
    667      for (let i = properties.length - 1; i >= 0; i--) {
    668        const property = properties[i];
    669        // We just need to find if a rule has this property while it matches
    670        // the viewedElement (or its parents).
    671        if (
    672          // check if the property is assigned
    673          (rule.isPropertyAssigned(property) ||
    674            // or if this is a css variable, if it's being used in the rule.
    675            (property.startsWith("--") &&
    676              // we may have false positive for dashed ident or the variable being
    677              // used in comment/string, but the tradeoff seems okay, as we would have
    678              // to parse the value of each declaration, which could be costly.
    679              new RegExp(`${property}[^A-Za-z0-9_-]`).test(getCssText()))) &&
    680          (status == STATUS.MATCHED ||
    681            (status == STATUS.PARENT_MATCH &&
    682              InspectorUtils.isInheritedProperty(
    683                this.viewedDocument,
    684                property
    685              )))
    686        ) {
    687          result.add(property);
    688          // Once the property has a matched selector, we can remove it from the array
    689          properties.splice(i, 1);
    690        }
    691      }
    692 
    693      if (!properties.length) {
    694        return result;
    695      }
    696    }
    697 
    698    return result;
    699  }
    700 
    701  /**
    702   * Build the array of matched rules for the currently highlighted element.
    703   * The array will hold rules that match the viewedElement and its parents.
    704   *
    705   * @private
    706   */
    707  #buildMatchedRules() {
    708    let domRules;
    709    let element = this.viewedElement;
    710    const filter = this.sourceFilter;
    711    let sheetIndex = 0;
    712 
    713    // distance is used to tell us how close an ancestor is to an element e.g.
    714    //  0: The rule is directly applied to the current element.
    715    // -1: The rule is inherited from the current element's first parent.
    716    // -2: The rule is inherited from the current element's second parent.
    717    // etc.
    718    let distance = 0;
    719 
    720    this.matchId++;
    721    this.passId++;
    722    this.#matchedRules = [];
    723 
    724    if (!element) {
    725      return;
    726    }
    727 
    728    do {
    729      const status =
    730        this.viewedElement === element ? STATUS.MATCHED : STATUS.PARENT_MATCH;
    731 
    732      try {
    733        domRules = getMatchingCSSRules(element);
    734      } catch (ex) {
    735        console.log("CL__buildMatchedRules error: " + ex);
    736        continue;
    737      }
    738 
    739      // getMatchingCSSRules can return null with a shadow DOM element.
    740      if (domRules !== null) {
    741        // getMatchingCSSRules returns ordered from least-specific to most-specific,
    742        // but we do want them from most-specific to least specific, so we need to loop
    743        // through the rules backward.
    744        for (let i = domRules.length - 1; i >= 0; i--) {
    745          const domRule = domRules[i];
    746          if (domRule.declarationOrigin) {
    747            // We only consume element.style and pres hint declarations for now
    748            if (
    749              domRule.declarationOrigin !== "style-attribute" &&
    750              domRule.declarationOrigin !== "pres-hints"
    751            ) {
    752              continue;
    753            }
    754 
    755            const rule = new CssRule(
    756              null,
    757              {
    758                style: domRule.style,
    759                declarationOrigin: domRule.declarationOrigin,
    760              },
    761              element
    762            );
    763            rule.matchId = this.matchId;
    764            rule.passId = this.passId;
    765            this.#matchedRules.push([rule, status, distance]);
    766 
    767            continue;
    768          }
    769          const sheet = this.getSheet(domRule.parentStyleSheet, -1);
    770          if (sheet.passId !== this.passId) {
    771            sheet.index = sheetIndex++;
    772            sheet.passId = this.passId;
    773          }
    774 
    775          if (filter === FILTER.USER && !sheet.authorSheet) {
    776            continue;
    777          }
    778 
    779          const rule = sheet.getRule(domRule);
    780          if (rule.passId === this.passId) {
    781            continue;
    782          }
    783 
    784          rule.matchId = this.matchId;
    785          rule.passId = this.passId;
    786          this.#matchedRules.push([rule, status, distance]);
    787        }
    788      }
    789 
    790      distance--;
    791    } while (
    792      // Loop on flattenedTreeParentNode instead of parentNode to reach the
    793      // shadow host from the shadow dom.
    794      (element = element.flattenedTreeParentNode) &&
    795      element.nodeType === nodeConstants.ELEMENT_NODE
    796    );
    797  }
    798 
    799  /**
    800   * Tells if the given DOM CSS object matches the current view media.
    801   *
    802   * @param {object} domObject The DOM CSS object to check.
    803   * @return {boolean} True if the DOM CSS object matches the current view
    804   * media, or false otherwise.
    805   */
    806  mediaMatches(domObject) {
    807    const mediaText = domObject.media.mediaText;
    808    return (
    809      !mediaText ||
    810      this.viewedDocument.defaultView.matchMedia(mediaText).matches
    811    );
    812  }
    813 }
    814 
    815 class CssSheet {
    816  /**
    817   * A safe way to access cached bits of information about a stylesheet.
    818   *
    819   * @class
    820   * @param {CssLogic} cssLogic pointer to the CssLogic instance working with
    821   * this CssSheet object.
    822   * @param {CSSStyleSheet} domSheet reference to a DOM CSSStyleSheet object.
    823   * @param {number} index tells the index/position of the stylesheet within the
    824   * main document.
    825   */
    826  constructor(cssLogic, domSheet, index) {
    827    this.#cssLogic = cssLogic;
    828    this.domSheet = domSheet;
    829    this.index = this.authorSheet ? index : -100 * index;
    830  }
    831 
    832  #cssLogic;
    833  // Cache of the sheets href. Cached by the getter.
    834  #href = null;
    835 
    836  // Short version of href for use in select boxes etc. Cached by getter.
    837  #shortSource = null;
    838 
    839  // Cached CssRules from the given stylesheet.
    840  #rules = {};
    841 
    842  // null for uncached.
    843  #sheetAllowed = null;
    844 
    845  #ruleCount = -1;
    846 
    847  passId = null;
    848  #agentSheet = null;
    849  #authorSheet = null;
    850  #userSheet = null;
    851 
    852  /**
    853   * Check if the stylesheet is an agent stylesheet (provided by the browser).
    854   *
    855   * @return {boolean} true if this is an agent stylesheet, false otherwise.
    856   */
    857  get agentSheet() {
    858    if (this.#agentSheet === null) {
    859      this.#agentSheet = isAgentStylesheet(this.domSheet);
    860    }
    861    return this.#agentSheet;
    862  }
    863 
    864  /**
    865   * Check if the stylesheet is an author stylesheet (provided by the content page).
    866   *
    867   * @return {boolean} true if this is an author stylesheet, false otherwise.
    868   */
    869  get authorSheet() {
    870    if (this.#authorSheet === null) {
    871      this.#authorSheet = isAuthorStylesheet(this.domSheet);
    872    }
    873    return this.#authorSheet;
    874  }
    875 
    876  /**
    877   * Check if the stylesheet is a user stylesheet (provided by userChrome.css or
    878   * userContent.css).
    879   *
    880   * @return {boolean} true if this is a user stylesheet, false otherwise.
    881   */
    882  get userSheet() {
    883    if (this.#userSheet === null) {
    884      this.#userSheet = isUserStylesheet(this.domSheet);
    885    }
    886    return this.#userSheet;
    887  }
    888 
    889  /**
    890   * Check if the stylesheet is disabled or not.
    891   *
    892   * @return {boolean} true if this stylesheet is disabled, or false otherwise.
    893   */
    894  get disabled() {
    895    return this.domSheet.disabled;
    896  }
    897 
    898  /**
    899   * Get a source for a stylesheet, using CssLogic.href
    900   *
    901   * @return {string} the address of the stylesheet.
    902   */
    903  get href() {
    904    if (this.#href) {
    905      return this.#href;
    906    }
    907 
    908    this.#href = CssLogic.href(this.domSheet);
    909    return this.#href;
    910  }
    911 
    912  /**
    913   * Create a shorthand version of the href of a stylesheet.
    914   *
    915   * @return {string} the shorthand source of the stylesheet.
    916   */
    917  get shortSource() {
    918    if (this.#shortSource) {
    919      return this.#shortSource;
    920    }
    921 
    922    this.#shortSource = shortSource(this.domSheet);
    923    return this.#shortSource;
    924  }
    925 
    926  /**
    927   * Tells if the sheet is allowed or not by the current CssLogic.sourceFilter.
    928   *
    929   * @return {boolean} true if the stylesheet is allowed by the sourceFilter, or
    930   * false otherwise.
    931   */
    932  get sheetAllowed() {
    933    if (this.#sheetAllowed !== null) {
    934      return this.#sheetAllowed;
    935    }
    936 
    937    this.#sheetAllowed = true;
    938 
    939    const filter = this.#cssLogic.sourceFilter;
    940    if (filter === FILTER.USER && !this.authorSheet) {
    941      this.#sheetAllowed = false;
    942    }
    943    if (filter !== FILTER.USER && filter !== FILTER.UA) {
    944      this.#sheetAllowed = filter === this.href;
    945    }
    946 
    947    return this.#sheetAllowed;
    948  }
    949 
    950  /**
    951   * Retrieve the number of rules in this stylesheet.
    952   *
    953   * @return {number} the number of CSSRule objects in this stylesheet.
    954   */
    955  get ruleCount() {
    956    try {
    957      return this.#ruleCount > -1 ? this.#ruleCount : this.getCssRules().length;
    958    } catch (e) {
    959      return 0;
    960    }
    961  }
    962 
    963  /**
    964   * Retrieve the array of css rules for this stylesheet.
    965   *
    966   * Accessing cssRules on a stylesheet that is not completely loaded can throw a
    967   * DOMException (Bug 625013). This wrapper will return an empty array instead.
    968   *
    969   * @return {Array} array of css rules.
    970   */
    971  getCssRules() {
    972    try {
    973      return this.domSheet.cssRules;
    974    } catch (e) {
    975      return [];
    976    }
    977  }
    978 
    979  /**
    980   * Retrieve a CssRule object for the given CSSStyleRule. The CssRule object is
    981   * cached, such that subsequent retrievals return the same CssRule object for
    982   * the same CSSStyleRule object.
    983   *
    984   * @param {CSSStyleRule} aDomRule the CSSStyleRule object for which you want a
    985   * CssRule object.
    986   * @return {CssRule} the cached CssRule object for the given CSSStyleRule
    987   * object.
    988   */
    989  getRule(domRule) {
    990    const cacheId = domRule.type + domRule.selectorText;
    991 
    992    let rule = null;
    993    let ruleFound = false;
    994 
    995    if (cacheId in this.#rules) {
    996      for (rule of this.#rules[cacheId]) {
    997        if (rule.domRule === domRule) {
    998          ruleFound = true;
    999          break;
   1000        }
   1001      }
   1002    }
   1003 
   1004    if (!ruleFound) {
   1005      if (!(cacheId in this.#rules)) {
   1006        this.#rules[cacheId] = [];
   1007      }
   1008 
   1009      rule = new CssRule(this, domRule);
   1010      this.#rules[cacheId].push(rule);
   1011    }
   1012 
   1013    return rule;
   1014  }
   1015 
   1016  toString() {
   1017    return "CssSheet[" + this.shortSource + "]";
   1018  }
   1019 }
   1020 
   1021 class CssRule {
   1022  /**
   1023   * Information about a single CSSStyleRule.
   1024   *
   1025   * @param {CSSStyleSheet|null} cssSheet the CssSheet object of the stylesheet that
   1026   * holds the CSSStyleRule. If the rule comes from element.style, set this
   1027   * argument to null.
   1028   * @param {CSSStyleRule|InspectorDeclaration} domRule the DOM CSSStyleRule for which you want
   1029   * to cache data. If the rule comes from element.style or presentational attributes, it
   1030   * will be an InspectorDeclaration (object of the form {style: element.style, declarationOrigin: string}).
   1031   * @param {Element} [element] If the rule comes from element.style, then this
   1032   * argument must point to the element.
   1033   * @class
   1034   */
   1035  constructor(cssSheet, domRule, element) {
   1036    this.#cssSheet = cssSheet;
   1037    this.domRule = domRule;
   1038 
   1039    if (this.#cssSheet) {
   1040      // parse domRule.selectorText on call to this.selectors
   1041      this.#selectors = null;
   1042      this.line = InspectorUtils.getRelativeRuleLine(this.domRule);
   1043      this.column = InspectorUtils.getRuleColumn(this.domRule);
   1044      this.href = this.#cssSheet.href;
   1045      this.authorRule = this.#cssSheet.authorSheet;
   1046      this.userRule = this.#cssSheet.userSheet;
   1047      this.agentRule = this.#cssSheet.agentSheet;
   1048    } else if (element) {
   1049      let selector = "";
   1050      if (domRule.declarationOrigin === "style-attribute") {
   1051        selector = "@element.style";
   1052      } else if (domRule.declarationOrigin === "pres-hints") {
   1053        selector = "@element.attributesStyle";
   1054      }
   1055 
   1056      this.#selectors = [new CssSelector(this, selector, 0)];
   1057      this.line = -1;
   1058      this.href = "#";
   1059      this.authorRule = true;
   1060      this.userRule = false;
   1061      this.agentRule = false;
   1062      this.sourceElement = element;
   1063    }
   1064  }
   1065 
   1066  passId = null;
   1067 
   1068  #cssSheet;
   1069  #selectors;
   1070 
   1071  /**
   1072   * Check if the parent stylesheet is allowed by the CssLogic.sourceFilter.
   1073   *
   1074   * @return {boolean} true if the parent stylesheet is allowed by the current
   1075   * sourceFilter, or false otherwise.
   1076   */
   1077  get sheetAllowed() {
   1078    return this.#cssSheet ? this.#cssSheet.sheetAllowed : true;
   1079  }
   1080 
   1081  /**
   1082   * Retrieve the parent stylesheet index/position in the viewed document.
   1083   *
   1084   * @return {number} the parent stylesheet index/position in the viewed
   1085   * document.
   1086   */
   1087  get sheetIndex() {
   1088    return this.#cssSheet ? this.#cssSheet.index : 0;
   1089  }
   1090 
   1091  /**
   1092   * Returns the underlying CSSRule style
   1093   *
   1094   * @returns CSS2Properties
   1095   */
   1096  getStyle() {
   1097    // When dealing with a style attribute "rule", this.domRule is a InspectorDeclaration
   1098    // and its style property might not reflect the current declarations, so we need to
   1099    // retrieve the source element style instead.
   1100    return this.domRule.declarationOrigin === "style-attribute"
   1101      ? this.sourceElement.style
   1102      : this.domRule.style;
   1103  }
   1104 
   1105  /**
   1106   * Retrieve the style property value from the current CSSStyleRule.
   1107   *
   1108   * @param {string} property the CSS property name for which you want the
   1109   * value.
   1110   * @return {string} the property value.
   1111   */
   1112  getPropertyValue(property) {
   1113    return this.getStyle().getPropertyValue(property);
   1114  }
   1115 
   1116  /**
   1117   * Returns whether or not the given property is set in the current CSSStyleRule.
   1118   *
   1119   * @param {string} property the CSS property name
   1120   * @return {boolean}
   1121   */
   1122  isPropertyAssigned(property) {
   1123    return this.getStyle().hasLonghandProperty(property);
   1124  }
   1125 
   1126  /**
   1127   * Retrieve the style property priority from the current CSSStyleRule.
   1128   *
   1129   * @param {string} property the CSS property name for which you want the
   1130   * priority.
   1131   * @return {string} the property priority.
   1132   */
   1133  getPropertyPriority(property) {
   1134    return this.getStyle().getPropertyPriority(property);
   1135  }
   1136 
   1137  /**
   1138   * Retrieve the list of CssSelector objects for each of the parsed selectors
   1139   * of the current CSSStyleRule.
   1140   *
   1141   * @return {Array} the array hold the CssSelector objects.
   1142   */
   1143  get selectors() {
   1144    if (this.#selectors) {
   1145      return this.#selectors;
   1146    }
   1147 
   1148    // Parse the CSSStyleRule.selectorText string.
   1149    this.#selectors = [];
   1150 
   1151    if (!this.domRule.selectorText) {
   1152      return this.#selectors;
   1153    }
   1154 
   1155    const selectors = CssLogic.getSelectors(this.domRule);
   1156 
   1157    for (let i = 0, len = selectors.length; i < len; i++) {
   1158      this.#selectors.push(new CssSelector(this, selectors[i], i));
   1159    }
   1160 
   1161    return this.#selectors;
   1162  }
   1163 
   1164  toString() {
   1165    return "[CssRule " + this.domRule.selectorText + "]";
   1166  }
   1167 }
   1168 
   1169 class CssSelector {
   1170  /**
   1171   * The CSS selector class allows us to document the ranking of various CSS
   1172   * selectors.
   1173   *
   1174   * @class
   1175   * @param {CssRule} cssRule the CssRule instance from where the selector comes.
   1176   * @param {string} selector The selector that we wish to investigate.
   1177   * @param {number} index The index of the selector within it's rule.
   1178   */
   1179  constructor(cssRule, selector, index) {
   1180    this.cssRule = cssRule;
   1181    this.text = selector;
   1182    this.inlineStyle = cssRule.domRule?.declarationOrigin === "style-attribute";
   1183    this.selectorIndex = index;
   1184  }
   1185 
   1186  matchId = null;
   1187 
   1188  #specificity = null;
   1189 
   1190  /**
   1191   * Retrieve the CssSelector source element, which is the source of the CssRule
   1192   * owning the selector. This is only available when the CssSelector comes from
   1193   * an element.style.
   1194   *
   1195   * @return {string} the source element selector.
   1196   */
   1197  get sourceElement() {
   1198    return this.cssRule.sourceElement;
   1199  }
   1200 
   1201  /**
   1202   * Retrieve the address of the CssSelector. This points to the address of the
   1203   * CssSheet owning this selector.
   1204   *
   1205   * @return {string} the address of the CssSelector.
   1206   */
   1207  get href() {
   1208    return this.cssRule.href;
   1209  }
   1210 
   1211  /**
   1212   * Check if the selector comes from an agent stylesheet (provided by the browser).
   1213   *
   1214   * @return {boolean} true if this is an agent stylesheet, false otherwise.
   1215   */
   1216  get agentRule() {
   1217    return this.cssRule.agentRule;
   1218  }
   1219 
   1220  /**
   1221   * Check if the selector comes from an author stylesheet (provided by the content page).
   1222   *
   1223   * @return {boolean} true if this is an author stylesheet, false otherwise.
   1224   */
   1225  get authorRule() {
   1226    return this.cssRule.authorRule;
   1227  }
   1228 
   1229  /**
   1230   * Check if the selector comes from a user stylesheet (provided by userChrome.css or
   1231   * userContent.css).
   1232   *
   1233   * @return {boolean} true if this is a user stylesheet, false otherwise.
   1234   */
   1235  get userRule() {
   1236    return this.cssRule.userRule;
   1237  }
   1238 
   1239  /**
   1240   * Check if the parent stylesheet is allowed by the CssLogic.sourceFilter.
   1241   *
   1242   * @return {boolean} true if the parent stylesheet is allowed by the current
   1243   * sourceFilter, or false otherwise.
   1244   */
   1245  get sheetAllowed() {
   1246    return this.cssRule.sheetAllowed;
   1247  }
   1248 
   1249  /**
   1250   * Retrieve the parent stylesheet index/position in the viewed document.
   1251   *
   1252   * @return {number} the parent stylesheet index/position in the viewed
   1253   * document.
   1254   */
   1255  get sheetIndex() {
   1256    return this.cssRule.sheetIndex;
   1257  }
   1258 
   1259  /**
   1260   * Retrieve the line of the parent CSSStyleRule in the parent CSSStyleSheet.
   1261   *
   1262   * @return {number} the line of the parent CSSStyleRule in the parent
   1263   * stylesheet.
   1264   */
   1265  get ruleLine() {
   1266    return this.cssRule.line;
   1267  }
   1268 
   1269  /**
   1270   * Retrieve the column of the parent CSSStyleRule in the parent CSSStyleSheet.
   1271   *
   1272   * @return {number} the column of the parent CSSStyleRule in the parent
   1273   * stylesheet.
   1274   */
   1275  get ruleColumn() {
   1276    return this.cssRule.column;
   1277  }
   1278 
   1279  /**
   1280   * Retrieve specificity information for the current selector.
   1281   *
   1282   * @see http://www.w3.org/TR/css3-selectors/#specificity
   1283   * @see http://www.w3.org/TR/CSS2/selector.html
   1284   *
   1285   * @return {number} The selector's specificity.
   1286   */
   1287  get specificity() {
   1288    if (this.inlineStyle) {
   1289      // We don't have an actual rule to call selectorSpecificityAt for element styles.
   1290      // However, specificity of element style is constant, 1,0,0,0 or 0x40000000,
   1291      // so just return the constant directly.
   1292      // @see http://www.w3.org/TR/CSS2/cascade.html#specificity
   1293      return 0x40000000;
   1294    }
   1295 
   1296    if (this.cssRule.declarationOrigin === "pres-hints") {
   1297      // As for element styles, we don't have an actual rule to call selectorSpecificityAt
   1298      // on for pres-hints styles.
   1299      // However, specificity of such "rule" is constant, 0,0,0,0, just return the constant
   1300      // directly. @see https://www.w3.org/TR/CSS2/cascade.html#preshint
   1301      return 0;
   1302    }
   1303 
   1304    if (typeof this.#specificity !== "number") {
   1305      this.#specificity = this.cssRule.domRule.selectorSpecificityAt(
   1306        this.selectorIndex
   1307      );
   1308    }
   1309 
   1310    return this.#specificity;
   1311  }
   1312 
   1313  toString() {
   1314    return this.text;
   1315  }
   1316 }
   1317 
   1318 class CssPropertyInfo {
   1319  /**
   1320   * A cache of information about the matched rules, selectors and values attached
   1321   * to a CSS property, for the highlighted element.
   1322   *
   1323   * The heart of the CssPropertyInfo object is the #findMatchedSelectors()
   1324   * method. This are invoked when the PropertyView tries to access the
   1325   * .matchedSelectors array.
   1326   * Results are cached, for later reuse.
   1327   *
   1328   * @param {CssLogic} cssLogic Reference to the parent CssLogic instance
   1329   * @param {string} property The CSS property we are gathering information for
   1330   * @class
   1331   */
   1332  constructor(cssLogic, property) {
   1333    this.#cssLogic = cssLogic;
   1334    this.property = property;
   1335  }
   1336 
   1337  // An array holding CssSelectorInfo objects for each of the matched selectors
   1338  // that are inside a CSS rule. Only rules that hold the this.property are
   1339  // counted. This includes rules that come from filtered stylesheets (those
   1340  // that have sheetAllowed = false).
   1341  #matchedSelectors = null;
   1342 
   1343  #cssLogic;
   1344  #value = "";
   1345 
   1346  /**
   1347   * Retrieve the computed style value for the current property, for the
   1348   * highlighted element.
   1349   *
   1350   * @return {string} the computed style value for the current property, for the
   1351   * highlighted element.
   1352   */
   1353  get value() {
   1354    if (!this.#value && this.#cssLogic.computedStyle) {
   1355      try {
   1356        this.#value = this.#cssLogic.computedStyle.getPropertyValue(
   1357          this.property
   1358        );
   1359      } catch (ex) {
   1360        console.log("Error reading computed style for " + this.property);
   1361        console.log(ex);
   1362      }
   1363    }
   1364    return this.#value;
   1365  }
   1366 
   1367  /**
   1368   * Retrieve the array holding CssSelectorInfo objects for each of the matched
   1369   * selectors, from each of the matched rules. Only selectors coming from
   1370   * allowed stylesheets are included in the array.
   1371   *
   1372   * @return {Array} the list of CssSelectorInfo objects of selectors that match
   1373   * the highlighted element and its parents.
   1374   */
   1375  get matchedSelectors() {
   1376    if (!this.#matchedSelectors) {
   1377      this.#findMatchedSelectors();
   1378    } else if (this.needRefilter) {
   1379      this.#refilterSelectors();
   1380    }
   1381 
   1382    return this.#matchedSelectors;
   1383  }
   1384 
   1385  /**
   1386   * Find the selectors that match the highlighted element and its parents.
   1387   * Uses CssLogic.processMatchedSelectors() to find the matched selectors,
   1388   * passing in a reference to CssPropertyInfo.#processMatchedSelector() to
   1389   * create CssSelectorInfo objects, which we then sort
   1390   *
   1391   * @private
   1392   */
   1393  #findMatchedSelectors() {
   1394    this.#matchedSelectors = [];
   1395    this.needRefilter = false;
   1396 
   1397    this.#cssLogic.processMatchedSelectors(this.#processMatchedSelector, this);
   1398 
   1399    // Sort the selectors by how well they match the given element.
   1400    this.#matchedSelectors.sort((selectorInfo1, selectorInfo2) =>
   1401      selectorInfo1.compareTo(selectorInfo2, this.#matchedSelectors)
   1402    );
   1403 
   1404    // Now we know which of the matches is best, we can mark it BEST_MATCH.
   1405    if (
   1406      this.#matchedSelectors.length &&
   1407      this.#matchedSelectors[0].status > STATUS.UNMATCHED
   1408    ) {
   1409      this.#matchedSelectors[0].status = STATUS.BEST;
   1410    }
   1411  }
   1412 
   1413  /**
   1414   * Process a matched CssSelector object.
   1415   *
   1416   * @private
   1417   * @param {CssSelector} selector: the matched CssSelector object.
   1418   * @param {STATUS} status: the CssSelector match status.
   1419   * @param {Int} distance: See CssLogic.#buildMatchedRules for definition.
   1420   */
   1421  #processMatchedSelector(selector, status, distance) {
   1422    const cssRule = selector.cssRule;
   1423    if (
   1424      cssRule.isPropertyAssigned(this.property) &&
   1425      (status == STATUS.MATCHED ||
   1426        (status == STATUS.PARENT_MATCH &&
   1427          InspectorUtils.isInheritedProperty(
   1428            this.#cssLogic.viewedDocument,
   1429            this.property
   1430          )))
   1431    ) {
   1432      const selectorInfo = new CssSelectorInfo(
   1433        selector,
   1434        this.property,
   1435        // FIXME: If this is a property that is coming from a longhand property which is
   1436        // using CSS variables, we would get an empty string at this point.
   1437        // It would be nice to try to display a value that would make sense to the user.
   1438        // See Bug 2003264
   1439        cssRule.getPropertyValue(this.property),
   1440        status,
   1441        distance
   1442      );
   1443      this.#matchedSelectors.push(selectorInfo);
   1444    }
   1445  }
   1446 
   1447  /**
   1448   * Refilter the matched selectors array when the CssLogic.sourceFilter
   1449   * changes. This allows for quick filter changes.
   1450   *
   1451   * @private
   1452   */
   1453  #refilterSelectors() {
   1454    const passId = ++this.#cssLogic.passId;
   1455 
   1456    const iterator = function (selectorInfo) {
   1457      const cssRule = selectorInfo.selector.cssRule;
   1458      if (cssRule.passId != passId) {
   1459        cssRule.passId = passId;
   1460      }
   1461    };
   1462 
   1463    if (this.#matchedSelectors) {
   1464      this.#matchedSelectors.forEach(iterator);
   1465    }
   1466 
   1467    this.needRefilter = false;
   1468  }
   1469 
   1470  toString() {
   1471    return "CssPropertyInfo[" + this.property + "]";
   1472  }
   1473 }
   1474 
   1475 class CssSelectorInfo {
   1476  /**
   1477   * A class that holds information about a given CssSelector object.
   1478   *
   1479   * Instances of this class are given to CssHtmlTree in the array of matched
   1480   * selectors. Each such object represents a displayable row in the PropertyView
   1481   * objects. The information given by this object blends data coming from the
   1482   * CssSheet, CssRule and from the CssSelector that own this object.
   1483   *
   1484   * @param {CssSelector} selector The CssSelector object for which to
   1485   *        present information.
   1486   * @param {string} property The property for which information should
   1487   *        be retrieved.
   1488   * @param {string} value The property value from the CssRule that owns
   1489   *        the selector.
   1490   * @param {STATUS} status The selector match status.
   1491   * @param {number} distance See CssLogic.#buildMatchedRules for definition.
   1492   */
   1493  constructor(selector, property, value, status, distance) {
   1494    this.selector = selector;
   1495    this.property = property;
   1496    this.status = status;
   1497    this.distance = distance;
   1498    this.value = value;
   1499    const priority = this.selector.cssRule.getPropertyPriority(this.property);
   1500    this.important = priority === "important";
   1501 
   1502    // Array<string|CSSLayerBlockRule>
   1503    this.parentLayers = [];
   1504 
   1505    // Go through all parent rules to populate this.parentLayers
   1506    let rule = selector.cssRule.domRule;
   1507    while (rule) {
   1508      const className = ChromeUtils.getClassName(rule);
   1509      if (className == "CSSLayerBlockRule") {
   1510        // If the layer has a name, it's enough to uniquely identify it
   1511        // If the layer does not have a name. We put the actual rule here, so we'll
   1512        // be able to compare actual rule instances in `compareTo`
   1513        this.parentLayers.push(rule.name || rule);
   1514      } else if (className == "CSSImportRule" && rule.layerName !== null) {
   1515        // Same reasoning for @import rule + layer
   1516        this.parentLayers.push(rule.layerName || rule);
   1517      }
   1518 
   1519      // Get the parent rule (could be the parent stylesheet owner rule
   1520      // for `@import url(path/to/file.css) layer`)
   1521      rule = rule.parentRule || rule.parentStyleSheet?.ownerRule;
   1522    }
   1523  }
   1524 
   1525  /**
   1526   * Retrieve the CssSelector source element, which is the source of the CssRule
   1527   * owning the selector. This is only available when the CssSelector comes from
   1528   * an element.style.
   1529   *
   1530   * @return {string} the source element selector.
   1531   */
   1532  get sourceElement() {
   1533    return this.selector.sourceElement;
   1534  }
   1535 
   1536  /**
   1537   * Retrieve the address of the CssSelector. This points to the address of the
   1538   * CssSheet owning this selector.
   1539   *
   1540   * @return {string} the address of the CssSelector.
   1541   */
   1542  get href() {
   1543    return this.selector.href;
   1544  }
   1545 
   1546  /**
   1547   * Check if the CssSelector comes from element.style or not.
   1548   *
   1549   * @return {boolean} true if the CssSelector comes from element.style, or
   1550   * false otherwise.
   1551   */
   1552  get inlineStyle() {
   1553    return this.selector.inlineStyle;
   1554  }
   1555 
   1556  /**
   1557   * Retrieve specificity information for the current selector.
   1558   *
   1559   * @return {object} an object holding specificity information for the current
   1560   * selector.
   1561   */
   1562  get specificity() {
   1563    return this.selector.specificity;
   1564  }
   1565 
   1566  /**
   1567   * Retrieve the parent stylesheet index/position in the viewed document.
   1568   *
   1569   * @return {number} the parent stylesheet index/position in the viewed
   1570   * document.
   1571   */
   1572  get sheetIndex() {
   1573    return this.selector.sheetIndex;
   1574  }
   1575 
   1576  /**
   1577   * Check if the parent stylesheet is allowed by the CssLogic.sourceFilter.
   1578   *
   1579   * @return {boolean} true if the parent stylesheet is allowed by the current
   1580   * sourceFilter, or false otherwise.
   1581   */
   1582  get sheetAllowed() {
   1583    return this.selector.sheetAllowed;
   1584  }
   1585 
   1586  /**
   1587   * Retrieve the line of the parent CSSStyleRule in the parent CSSStyleSheet.
   1588   *
   1589   * @return {number} the line of the parent CSSStyleRule in the parent
   1590   * stylesheet.
   1591   */
   1592  get ruleLine() {
   1593    return this.selector.ruleLine;
   1594  }
   1595 
   1596  /**
   1597   * Retrieve the column of the parent CSSStyleRule in the parent CSSStyleSheet.
   1598   *
   1599   * @return {number} the column of the parent CSSStyleRule in the parent
   1600   * stylesheet.
   1601   */
   1602  get ruleColumn() {
   1603    return this.selector.ruleColumn;
   1604  }
   1605 
   1606  /**
   1607   * Check if the selector comes from a browser-provided stylesheet.
   1608   *
   1609   * @return {boolean} true if the selector comes from a browser-provided
   1610   * stylesheet, or false otherwise.
   1611   */
   1612  get agentRule() {
   1613    return this.selector.agentRule;
   1614  }
   1615 
   1616  /**
   1617   * Check if the selector comes from a webpage-provided stylesheet.
   1618   *
   1619   * @return {boolean} true if the selector comes from a webpage-provided
   1620   * stylesheet, or false otherwise.
   1621   */
   1622  get authorRule() {
   1623    return this.selector.authorRule;
   1624  }
   1625 
   1626  /**
   1627   * Check if the selector comes from a user stylesheet (userChrome.css or
   1628   * userContent.css).
   1629   *
   1630   * @return {boolean} true if the selector comes from a webpage-provided
   1631   * stylesheet, or false otherwise.
   1632   */
   1633  get userRule() {
   1634    return this.selector.userRule;
   1635  }
   1636 
   1637  /**
   1638   * Compare the current CssSelectorInfo instance to another instance.
   1639   * Since selectorInfos is computed from `InspectorUtils.getMatchingCSSRules`,
   1640   * it's already sorted for regular cases. We only need to handle important values.
   1641   *
   1642   * @param  {CssSelectorInfo} that
   1643   *         The instance to compare ourselves against.
   1644   * @param  {Array<CssSelectorInfo>} selectorInfos
   1645   *         The list of CssSelectorInfo we are currently ordering
   1646   * @return {number}
   1647   *         -1, 0, 1 depending on how that compares with this.
   1648   */
   1649  compareTo(that, selectorInfos) {
   1650    const originalOrder =
   1651      selectorInfos.indexOf(this) < selectorInfos.indexOf(that) ? -1 : 1;
   1652 
   1653    // If both properties are not important, we can keep the original order
   1654    if (!this.important && !that.important) {
   1655      return originalOrder;
   1656    }
   1657 
   1658    // If one of the property is important and the other is not, the important one wins
   1659    if (this.important !== that.important) {
   1660      return this.important ? -1 : 1;
   1661    }
   1662 
   1663    // At this point, this and that are both important
   1664 
   1665    const thisIsInLayer = !!this.parentLayers.length;
   1666    const thatIsInLayer = !!that.parentLayers.length;
   1667 
   1668    // If they're not in layers, we can keep the original rule order
   1669    if (!thisIsInLayer && !thatIsInLayer) {
   1670      return originalOrder;
   1671    }
   1672 
   1673    // If one of the rule is the style attribute, it wins
   1674    if (this.selector.inlineStyle || that.selector.inlineStyle) {
   1675      return this.selector.inlineStyle ? -1 : 1;
   1676    }
   1677 
   1678    // If one of the rule is not in a layer, then the rule in a layer wins.
   1679    if (!thisIsInLayer || !thatIsInLayer) {
   1680      return thisIsInLayer ? -1 : 1;
   1681    }
   1682 
   1683    const inSameLayers =
   1684      this.parentLayers.length === that.parentLayers.length &&
   1685      this.parentLayers.every((layer, i) => layer === that.parentLayers[i]);
   1686    // If both rules are in the same layer, we keep the original order
   1687    if (inSameLayers) {
   1688      return originalOrder;
   1689    }
   1690 
   1691    // When comparing declarations that belong to different layers, then for
   1692    // important rules the declaration whose cascade layer is first wins.
   1693    // We get the rules in the most-specific to least-specific order, meaning we'll have
   1694    // rules in layers in the reverse order of the order of declarations of layers.
   1695    // We can reverse that again to get the order of declarations of layers.
   1696    return originalOrder * -1;
   1697  }
   1698 
   1699  compare(that, propertyName, type) {
   1700    switch (type) {
   1701      case COMPAREMODE.BOOLEAN:
   1702        if (this[propertyName] && !that[propertyName]) {
   1703          return -1;
   1704        }
   1705        if (!this[propertyName] && that[propertyName]) {
   1706          return 1;
   1707        }
   1708        break;
   1709      case COMPAREMODE.INTEGER:
   1710        if (this[propertyName] > that[propertyName]) {
   1711          return -1;
   1712        }
   1713        if (this[propertyName] < that[propertyName]) {
   1714          return 1;
   1715        }
   1716        break;
   1717    }
   1718    return 0;
   1719  }
   1720 
   1721  toString() {
   1722    return this.selector + " -> " + this.value;
   1723  }
   1724 }
   1725 
   1726 exports.CssLogic = CssLogic;
   1727 exports.CssSelector = CssSelector;