tor-browser

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

stylesheets-manager.js (34781B)


      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 EventEmitter = require("resource://devtools/shared/event-emitter.js");
      8 const {
      9  getSourcemapBaseURL,
     10 } = require("resource://devtools/server/actors/utils/source-map-utils.js");
     11 
     12 loader.lazyRequireGetter(
     13  this,
     14  ["addPseudoClassLock", "removePseudoClassLock"],
     15  "resource://devtools/server/actors/highlighters/utils/markup.js",
     16  true
     17 );
     18 loader.lazyRequireGetter(
     19  this,
     20  "loadSheet",
     21  "resource://devtools/shared/layout/utils.js",
     22  true
     23 );
     24 loader.lazyRequireGetter(
     25  this,
     26  ["getStyleSheetOwnerNode", "getStyleSheetText"],
     27  "resource://devtools/server/actors/utils/stylesheet-utils.js",
     28  true
     29 );
     30 
     31 const TRANSITION_PSEUDO_CLASS = ":-moz-styleeditor-transitioning";
     32 const TRANSITION_DURATION_MS = 500;
     33 const TRANSITION_BUFFER_MS = 1000;
     34 const TRANSITION_RULE_SELECTOR = `:root${TRANSITION_PSEUDO_CLASS}, :root${TRANSITION_PSEUDO_CLASS} *:not(:-moz-native-anonymous)`;
     35 const TRANSITION_SHEET =
     36  "data:text/css;charset=utf-8," +
     37  encodeURIComponent(`
     38  ${TRANSITION_RULE_SELECTOR} {
     39    transition-duration: ${TRANSITION_DURATION_MS}ms !important;
     40    transition-delay: 0ms !important;
     41    transition-timing-function: ease-out !important;
     42    transition-property: all !important;
     43  }
     44 `);
     45 
     46 // The possible kinds of style-applied events.
     47 // UPDATE_PRESERVING_RULES means that the update is guaranteed to
     48 // preserve the number and order of rules on the style sheet.
     49 // UPDATE_GENERAL covers any other kind of change to the style sheet.
     50 const UPDATE_PRESERVING_RULES = 0;
     51 const UPDATE_GENERAL = 1;
     52 
     53 // If the user edits a stylesheet, we stash a copy of the edited text
     54 // here, keyed by the stylesheet.  This way, if the tools are closed
     55 // and then reopened, the edited text will be available. A weak map
     56 // is used so that navigation by the user will eventually cause the
     57 // edited text to be collected.
     58 const modifiedStyleSheets = new WeakMap();
     59 
     60 /**
     61 * Manage stylesheets related to a given Target Actor.
     62 *
     63 * @fires stylesheet-updated: emitted when there was changes in a stylesheet
     64 *        First arg is an object with the following properties:
     65 *        - resourceId {String}: The id that was assigned to the stylesheet
     66 *        - updateKind {String}: Which kind of update it is ("style-applied",
     67 *          "at-rules-changed", "matches-change", "property-change")
     68 *        - updates {Object}: The update data
     69 */
     70 class StyleSheetsManager extends EventEmitter {
     71  #abortController;
     72  // Map<resourceId, AbortController>
     73  #mqlChangeAbortControllerMap = new Map();
     74  #styleSheetCount = 0;
     75  #styleSheetMap = new Map();
     76  #styleSheetCreationData;
     77  #targetActor;
     78  #transitionSheetLoaded;
     79  #transitionTimeout;
     80  #watchListeners = {
     81    onAvailable: [],
     82    onUpdated: [],
     83    onDestroyed: [],
     84  };
     85 
     86  /**
     87   * @param TargetActor targetActor
     88   *        The target actor from which we should observe stylesheet changes.
     89   */
     90  constructor(targetActor) {
     91    super();
     92 
     93    this.#targetActor = targetActor;
     94  }
     95 
     96  #setEventListenersIfNeeded() {
     97    if (this.#abortController) {
     98      return;
     99    }
    100 
    101    this.#abortController = new AbortController();
    102    const { signal } = this.#abortController;
    103 
    104    // Listen for new stylesheet being added via StyleSheetApplicableStateChanged
    105    if (this.#targetActor.chromeEventHandler) {
    106      this.#targetActor.chromeEventHandler.addEventListener(
    107        "StyleSheetApplicableStateChanged",
    108        this.#onApplicableStateChanged,
    109        { capture: true, signal }
    110      );
    111      this.#targetActor.chromeEventHandler.addEventListener(
    112        "StyleSheetRemoved",
    113        this.#onStylesheetRemoved,
    114        { capture: true, signal }
    115      );
    116    }
    117 
    118    this.#watchStyleSheetChangeEvents();
    119    this.#targetActor.on("window-ready", this.#onTargetActorWindowReady, {
    120      signal,
    121    });
    122  }
    123 
    124  /**
    125   * Calling this function will make the StyleSheetsManager start the event listeners needed
    126   * to watch for stylesheet additions and modifications.
    127   * This resolves once it notified about existing stylesheets.
    128   *
    129   * @param {object} options
    130   * @param {Function} onAvailable: Function that will be called when a stylesheet is
    131   *                   registered, but also with already registered stylesheets
    132   *                   if ignoreExisting is not set to true.
    133   *                   This is called with a single object parameter with the following properties:
    134   *                   - {String} resourceId: The id that was assigned to the stylesheet
    135   *                   - {StyleSheet} styleSheet: The actual stylesheet object
    136   *                   - {Object} creationData: An object with:
    137   *                              - {boolean} isCreatedByDevTools: Was the stylesheet created
    138   *                                by DevTools (e.g. by the user clicking the new stylesheet
    139   *                                button in the styleeditor)
    140   *                              - {String} fileName
    141   * @param {Function} onUpdated: Function that will be called when a stylesheet is updated
    142   *                   This is called with a single object parameter with the following properties:
    143   *                   - {String} resourceId: The id that was assigned to the stylesheet
    144   *                   - {String} updateKind: Which kind of update it is ("style-applied",
    145   *                     "at-rules-changed", "matches-change", "property-change")
    146   *                   - {Object} updates : The update data
    147   * @param {Function} onDestroyed: Function that will be called when a stylesheet is removed
    148   *                   This is called with a single object parameter with the following properties:
    149   *                   - {String} resourceId: The id that was assigned to the stylesheet
    150   * @param {boolean} ignoreExisting: Pass to true to avoid onAvailable to be called with
    151   *                  already registered stylesheets.
    152   */
    153  async watch({ onAvailable, onUpdated, onDestroyed, ignoreExisting = false }) {
    154    if (!onAvailable && !onUpdated && !onDestroyed) {
    155      throw new Error("Expect onAvailable, onUpdated or onDestroyed");
    156    }
    157 
    158    if (onAvailable) {
    159      if (typeof onAvailable !== "function") {
    160        throw new Error("onAvailable should be a function");
    161      }
    162 
    163      // Don't register the listener yet if we're ignoring existing stylesheets, we'll do
    164      // that at the end of the function, after we processed existing stylesheets.
    165    }
    166 
    167    if (onUpdated) {
    168      if (typeof onUpdated !== "function") {
    169        throw new Error("onUpdated should be a function");
    170      }
    171      this.#watchListeners.onUpdated.push(onUpdated);
    172    }
    173 
    174    if (onDestroyed) {
    175      if (typeof onDestroyed !== "function") {
    176        throw new Error("onDestroyed should be a function");
    177      }
    178      this.#watchListeners.onDestroyed.push(onDestroyed);
    179    }
    180 
    181    // Process existing stylesheets
    182    const promises = [];
    183    for (const window of this.#targetActor.windows) {
    184      promises.push(this.#getStyleSheetsForWindow(window));
    185    }
    186 
    187    this.#setEventListenersIfNeeded();
    188 
    189    // Finally, notify about existing stylesheets
    190    const styleSheets = await Promise.all(promises);
    191    const styleSheetsData = styleSheets.flat().map(styleSheet => ({
    192      styleSheet,
    193      resourceId: this.#registerStyleSheet(styleSheet),
    194    }));
    195 
    196    let registeredStyleSheetsPromises;
    197    if (onAvailable && ignoreExisting !== true) {
    198      registeredStyleSheetsPromises = styleSheetsData.map(
    199        ({ resourceId, styleSheet }) => onAvailable({ resourceId, styleSheet })
    200      );
    201    }
    202 
    203    // Only register the listener after we went over the list of existing stylesheets
    204    // so the listener is not triggered by possible calls to #registerStyleSheet earlier.
    205    if (onAvailable) {
    206      this.#watchListeners.onAvailable.push(onAvailable);
    207    }
    208 
    209    if (registeredStyleSheetsPromises) {
    210      await Promise.all(registeredStyleSheetsPromises);
    211    }
    212  }
    213 
    214  /**
    215   * Remove the passed listeners
    216   *
    217   * @param {object} options: See this.watch
    218   */
    219  unwatch({ onAvailable, onUpdated, onDestroyed }) {
    220    if (!this.#watchListeners) {
    221      return;
    222    }
    223 
    224    if (onAvailable) {
    225      const index = this.#watchListeners.onAvailable.indexOf(onAvailable);
    226      if (index !== -1) {
    227        this.#watchListeners.onAvailable.splice(index, 1);
    228      }
    229    }
    230 
    231    if (onUpdated) {
    232      const index = this.#watchListeners.onUpdated.indexOf(onUpdated);
    233      if (index !== -1) {
    234        this.#watchListeners.onUpdated.splice(index, 1);
    235      }
    236    }
    237 
    238    if (onDestroyed) {
    239      const index = this.#watchListeners.onDestroyed.indexOf(onDestroyed);
    240      if (index !== -1) {
    241        this.#watchListeners.onDestroyed.splice(index, 1);
    242      }
    243    }
    244  }
    245 
    246  #watchStyleSheetChangeEvents() {
    247    for (const window of this.#targetActor.windows) {
    248      this.#watchStyleSheetChangeEventsForWindow(window);
    249    }
    250  }
    251 
    252  #onTargetActorWindowReady = ({ window }) => {
    253    this.#watchStyleSheetChangeEventsForWindow(window);
    254  };
    255 
    256  #watchStyleSheetChangeEventsForWindow(window) {
    257    // We have to set this flag in order to get the
    258    // StyleSheetApplicableStateChanged and StyleSheetRemoved events. See Document.webidl.
    259    window.document.styleSheetChangeEventsEnabled = true;
    260  }
    261 
    262  #unwatchStyleSheetChangeEvents() {
    263    for (const window of this.#targetActor.windows) {
    264      window.document.styleSheetChangeEventsEnabled = false;
    265    }
    266  }
    267 
    268  /**
    269   * Create a new style sheet in the document with the given text.
    270   *
    271   * @param  {Document} document
    272   *         Document that the new style sheet belong to.
    273   * @param  {Element} parent
    274   *         The element into which we'll append the <style> element
    275   * @param  {string} text
    276   *         Content of style sheet.
    277   * @param  {string} fileName
    278   *         If the stylesheet adding is from file, `fileName` indicates the path.
    279   */
    280  async addStyleSheet(document, parent, text, fileName) {
    281    const style = document.createElementNS(
    282      "http://www.w3.org/1999/xhtml",
    283      "style"
    284    );
    285    style.setAttribute("type", "text/css");
    286    style.setDevtoolsAsTriggeringPrincipal();
    287 
    288    if (text) {
    289      style.appendChild(document.createTextNode(text));
    290    }
    291 
    292    // This triggers StyleSheetApplicableStateChanged event.
    293    parent.appendChild(style);
    294 
    295    // This promise will be resolved when the resource for this stylesheet is available.
    296    let resolve = null;
    297    const promise = new Promise(r => {
    298      resolve = r;
    299    });
    300 
    301    if (!this.#styleSheetCreationData) {
    302      this.#styleSheetCreationData = new WeakMap();
    303    }
    304    this.#styleSheetCreationData.set(style.sheet, {
    305      isCreatedByDevTools: true,
    306      fileName,
    307      resolve,
    308    });
    309 
    310    await promise;
    311 
    312    return style.sheet;
    313  }
    314 
    315  /**
    316   * Return resourceId of the given style sheet or create one if the stylesheet wasn't
    317   * registered yet.
    318   *
    319   * @param {StyleSheet} styleSheet
    320   * @returns {string} resourceId
    321   */
    322  getStyleSheetResourceId(styleSheet) {
    323    const existingResourceId = this.#findStyleSheetResourceId(styleSheet);
    324    if (existingResourceId) {
    325      return existingResourceId;
    326    }
    327 
    328    // If we couldn't find an associated resourceId, that means the stylesheet isn't
    329    // registered yet. Calling #registerStyleSheet will register it and return the
    330    // associated resourceId it computed for it.
    331    return this.#registerStyleSheet(styleSheet);
    332  }
    333 
    334  /**
    335   * Return the associated resourceId of the given registered style sheet, or null if the
    336   * stylesheet wasn't registered yet.
    337   *
    338   * @param {StyleSheet} styleSheet
    339   * @returns {string} resourceId
    340   */
    341  #findStyleSheetResourceId(styleSheet) {
    342    for (const [
    343      resourceId,
    344      existingStyleSheet,
    345    ] of this.#styleSheetMap.entries()) {
    346      if (styleSheet === existingStyleSheet) {
    347        return resourceId;
    348      }
    349    }
    350 
    351    return null;
    352  }
    353 
    354  /**
    355   * Return owner node of the style sheet of the given resource id.
    356   *
    357   * @param {string} resourceId
    358   *                  The id associated with the stylesheet
    359   * @returns {Element|null}
    360   */
    361  getOwnerNode(resourceId) {
    362    const styleSheet = this.#styleSheetMap.get(resourceId);
    363    return styleSheet.ownerNode;
    364  }
    365 
    366  /**
    367   * Return the index of given stylesheet of the given resource id.
    368   *
    369   * @param {string} resourceId
    370   *                  The id associated with the stylesheet
    371   * @returns {number}
    372   */
    373  getStyleSheetIndex(resourceId) {
    374    const styleSheet = this.#styleSheetMap.get(resourceId);
    375 
    376    const styleSheets = InspectorUtils.getAllStyleSheets(
    377      this.#targetActor.window.document,
    378      true
    379    );
    380    let i = 0;
    381    for (const sheet of styleSheets) {
    382      if (!this.#shouldListSheet(sheet)) {
    383        continue;
    384      }
    385      if (sheet == styleSheet) {
    386        return i;
    387      }
    388      i++;
    389    }
    390    return -1;
    391  }
    392 
    393  /**
    394   * Get the text of a stylesheet given its resourceId.
    395   *
    396   * @param {string} resourceId
    397   *                  The id associated with the stylesheet
    398   * @returns {string}
    399   */
    400  async getText(resourceId) {
    401    const styleSheet = this.#styleSheetMap.get(resourceId);
    402 
    403    const modifiedText = modifiedStyleSheets.get(styleSheet);
    404 
    405    // modifiedText is the content of the stylesheet updated by update function.
    406    // In case not updating, this is undefined.
    407    if (modifiedText !== undefined) {
    408      return modifiedText;
    409    }
    410 
    411    return getStyleSheetText(styleSheet);
    412  }
    413 
    414  /**
    415   * Toggle the disabled property of the stylesheet
    416   *
    417   * @param {string} resourceId
    418   *                  The id associated with the stylesheet
    419   * @return {boolean} the disabled state after toggling.
    420   */
    421  toggleDisabled(resourceId) {
    422    const styleSheet = this.#styleSheetMap.get(resourceId);
    423    styleSheet.disabled = !styleSheet.disabled;
    424 
    425    this.#notifyPropertyChanged(resourceId, "disabled", styleSheet.disabled);
    426 
    427    return styleSheet.disabled;
    428  }
    429 
    430  /**
    431   * Update the style sheet in place with new text.
    432   *
    433   * @param  {string} resourceId
    434   * @param  {string} text
    435   *         New text.
    436   * @param  {object} options
    437   * @param  {boolean} options.transition
    438   *         Whether to do CSS transition for change. Defaults to false.
    439   * @param  {number} options.kind
    440   *         Either UPDATE_PRESERVING_RULES or UPDATE_GENERAL. Defaults to UPDATE_GENERAL.
    441   * @param {string} options.cause
    442   *         Indicates the cause of this update (e.g. "styleeditor") if this was called
    443   *         from the stylesheet to be edited by the user from the StyleEditor.
    444   */
    445  async setStyleSheetText(
    446    resourceId,
    447    text,
    448    { transition = false, kind = UPDATE_GENERAL, cause = "" } = {}
    449  ) {
    450    const styleSheet = this.#styleSheetMap.get(resourceId);
    451    InspectorUtils.parseStyleSheet(styleSheet, text);
    452    modifiedStyleSheets.set(styleSheet, text);
    453 
    454    // getStyleSheetRuleCountAndAtRules can be costly, so only call it when needed,
    455    // i.e. when the whole stylesheet is modified, not when a rule body is.
    456    let atRules, ruleCount;
    457    if (kind !== UPDATE_PRESERVING_RULES) {
    458      ({ atRules, ruleCount } =
    459        this.getStyleSheetRuleCountAndAtRules(styleSheet));
    460      this.#notifyPropertyChanged(resourceId, "ruleCount", ruleCount);
    461    }
    462 
    463    if (transition) {
    464      this.#startTransition(resourceId, kind, cause);
    465    } else {
    466      this.#onStyleSheetUpdated({
    467        resourceId,
    468        updateKind: "style-applied",
    469        updates: {
    470          event: { kind, cause },
    471        },
    472      });
    473    }
    474 
    475    if (kind !== UPDATE_PRESERVING_RULES) {
    476      this.#onStyleSheetUpdated({
    477        resourceId,
    478        updateKind: "at-rules-changed",
    479        updates: {
    480          resourceUpdates: { atRules },
    481        },
    482      });
    483    }
    484  }
    485 
    486  /**
    487   * Applies a transition to the stylesheet document so any change made by the user in the
    488   * client will be animated so it's more visible.
    489   *
    490   * @param {string} resourceId
    491   *        The id associated with the stylesheet
    492   * @param {number} kind
    493   *        Either UPDATE_PRESERVING_RULES or UPDATE_GENERAL
    494   * @param {string} cause
    495   *         Indicates the cause of this update (e.g. "styleeditor") if this was called
    496   *         from the stylesheet to be edited by the user from the StyleEditor.
    497   */
    498  #startTransition(resourceId, kind, cause) {
    499    const styleSheet = this.#styleSheetMap.get(resourceId);
    500    const document = styleSheet.associatedDocument;
    501    const window = document.ownerGlobal;
    502 
    503    if (!this.#transitionSheetLoaded) {
    504      this.#transitionSheetLoaded = true;
    505      // We don't remove this sheet. It uses an internal selector that
    506      // we only apply via locks, so there's no need to load and unload
    507      // it all the time.
    508      loadSheet(window, TRANSITION_SHEET);
    509    }
    510 
    511    addPseudoClassLock(document.documentElement, TRANSITION_PSEUDO_CLASS);
    512 
    513    // Set up clean up and commit after transition duration (+buffer)
    514    // @see #onTransitionEnd
    515    window.clearTimeout(this.#transitionTimeout);
    516    this.#transitionTimeout = window.setTimeout(
    517      this.#onTransitionEnd.bind(this, resourceId, kind, cause),
    518      TRANSITION_DURATION_MS + TRANSITION_BUFFER_MS
    519    );
    520  }
    521 
    522  /**
    523   * @param {string} resourceId
    524   *        The id associated with the stylesheet
    525   * @param {number} kind
    526   *        Either UPDATE_PRESERVING_RULES or UPDATE_GENERAL
    527   * @param {string} cause
    528   *         Indicates the cause of this update (e.g. "styleeditor") if this was called
    529   *         from the stylesheet to be edited by the user from the StyleEditor.
    530   */
    531  #onTransitionEnd(resourceId, kind, cause) {
    532    const styleSheet = this.#styleSheetMap.get(resourceId);
    533    const document = styleSheet.associatedDocument;
    534 
    535    this.#transitionTimeout = null;
    536    removePseudoClassLock(document.documentElement, TRANSITION_PSEUDO_CLASS);
    537 
    538    this.#onStyleSheetUpdated({
    539      resourceId,
    540      updateKind: "style-applied",
    541      updates: {
    542        event: { kind, cause },
    543      },
    544    });
    545  }
    546 
    547  /**
    548   * Retrieve the CSSRuleList of a given stylesheet
    549   *
    550   * @param {StyleSheet} styleSheet
    551   * @returns {CSSRuleList}
    552   */
    553  #getCSSRules(styleSheet) {
    554    try {
    555      return styleSheet.cssRules;
    556    } catch (e) {
    557      // sheet isn't loaded yet
    558    }
    559 
    560    if (!styleSheet.ownerNode) {
    561      return Promise.resolve([]);
    562    }
    563 
    564    return new Promise(resolve => {
    565      styleSheet.ownerNode.addEventListener(
    566        "load",
    567        () => resolve(styleSheet.cssRules),
    568        { once: true }
    569      );
    570    });
    571  }
    572 
    573  /**
    574   * Get the stylesheets imported by a given stylesheet (via @import)
    575   *
    576   * @param {Document} document
    577   * @param {StyleSheet} styleSheet
    578   * @returns Array<StyleSheet>
    579   */
    580  async #getImportedStyleSheets(document, styleSheet) {
    581    const importedStyleSheets = [];
    582 
    583    for (const rule of await this.#getCSSRules(styleSheet)) {
    584      const ruleClassName = ChromeUtils.getClassName(rule);
    585      if (ruleClassName == "CSSImportRule") {
    586        // With the Gecko style system, the associated styleSheet may be null
    587        // if it has already been seen because an import cycle for the same
    588        // URL.  With Stylo, the styleSheet will exist (which is correct per
    589        // the latest CSSOM spec), so we also need to check ancestors for the
    590        // same URL to avoid cycles.
    591        if (
    592          !rule.styleSheet ||
    593          this.#haveAncestorWithSameURL(rule.styleSheet) ||
    594          !this.#shouldListSheet(rule.styleSheet)
    595        ) {
    596          continue;
    597        }
    598 
    599        importedStyleSheets.push(rule.styleSheet);
    600 
    601        // recurse imports in this stylesheet as well
    602        const children = await this.#getImportedStyleSheets(
    603          document,
    604          rule.styleSheet
    605        );
    606        importedStyleSheets.push(...children);
    607      } else if (ruleClassName != "CSSCharsetRule") {
    608        // @import rules must precede all others except @charset
    609        break;
    610      }
    611    }
    612 
    613    return importedStyleSheets;
    614  }
    615 
    616  /**
    617   * Retrieve the total number of rules (including nested ones) and
    618   * all the at-rules of a given stylesheet.
    619   *
    620   * @param {StyleSheet} styleSheet
    621   * @returns {object} An object of the following shape:
    622   *          - {Integer} ruleCount: The total number of rules in the stylesheet
    623   *          - {Array<Object>} atRules: An array of object of the following shape:
    624   *            - type {String}
    625   *            - conditionText {String}
    626   *            - matches {Boolean}: true if the media rule matches the current state of the document
    627   *            - layerName {String}
    628   *            - line {Number}
    629   *            - column {Number}
    630   */
    631  getStyleSheetRuleCountAndAtRules(styleSheet) {
    632    const resourceId = this.#findStyleSheetResourceId(styleSheet);
    633    if (!resourceId) {
    634      return [];
    635    }
    636 
    637    if (this.#mqlChangeAbortControllerMap.has(resourceId)) {
    638      this.#mqlChangeAbortControllerMap.get(resourceId).abort();
    639      this.#mqlChangeAbortControllerMap.delete(resourceId);
    640    }
    641 
    642    // Accessing the stylesheet associated window might be slow due to cross compartment
    643    // wrappers, so only retrieve it if it's needed.
    644    let win;
    645    const getStyleSheetAssociatedWindow = () => {
    646      if (!win) {
    647        win = styleSheet.associatedDocument?.ownerGlobal;
    648      }
    649      return win;
    650    };
    651 
    652    // This returns the following type of at-rules:
    653    // - CSSMediaRule
    654    // - CSSContainerRule
    655    // - CSSSupportsRule
    656    // - CSSLayerBlockRule
    657    // New types can be added from InpsectorUtils.cpp `CollectAtRules`
    658    const { atRules: styleSheetRules, ruleCount } =
    659      InspectorUtils.getStyleSheetRuleCountAndAtRules(styleSheet);
    660    const atRules = [];
    661    for (const rule of styleSheetRules) {
    662      const className = ChromeUtils.getClassName(rule);
    663      if (className === "CSSMediaRule") {
    664        let matches = false;
    665 
    666        try {
    667          const associatedWin = getStyleSheetAssociatedWindow();
    668          const mql = associatedWin.matchMedia(rule.media.mediaText);
    669          matches = mql.matches;
    670 
    671          let ac = this.#mqlChangeAbortControllerMap.get(resourceId);
    672          if (!ac) {
    673            ac = new associatedWin.AbortController();
    674            this.#mqlChangeAbortControllerMap.set(resourceId, ac);
    675          }
    676 
    677          const index = atRules.length;
    678          mql.addEventListener(
    679            "change",
    680            () => this.#onMatchesChange(resourceId, index, mql),
    681            {
    682              signal: ac.signal,
    683            }
    684          );
    685        } catch (e) {
    686          // Ignored
    687        }
    688 
    689        atRules.push({
    690          type: "media",
    691          conditionText: rule.conditionText,
    692          matches,
    693          line: InspectorUtils.getRelativeRuleLine(rule),
    694          column: InspectorUtils.getRuleColumn(rule),
    695        });
    696      } else if (className === "CSSContainerRule") {
    697        atRules.push({
    698          type: "container",
    699          conditionText: rule.conditionText,
    700          line: InspectorUtils.getRelativeRuleLine(rule),
    701          column: InspectorUtils.getRuleColumn(rule),
    702        });
    703      } else if (className === "CSSSupportsRule") {
    704        atRules.push({
    705          type: "support",
    706          conditionText: rule.conditionText,
    707          line: InspectorUtils.getRelativeRuleLine(rule),
    708          column: InspectorUtils.getRuleColumn(rule),
    709        });
    710      } else if (className === "CSSLayerBlockRule") {
    711        atRules.push({
    712          type: "layer",
    713          layerName: rule.name,
    714          line: InspectorUtils.getRelativeRuleLine(rule),
    715          column: InspectorUtils.getRuleColumn(rule),
    716        });
    717      } else if (className === "CSSPropertyRule") {
    718        atRules.push({
    719          type: "property",
    720          propertyName: rule.name,
    721          line: InspectorUtils.getRelativeRuleLine(rule),
    722          column: InspectorUtils.getRuleColumn(rule),
    723        });
    724      } else if (className === "CSSPositionTryRule") {
    725        atRules.push({
    726          type: "position-try",
    727          positionTryName: rule.name,
    728          line: InspectorUtils.getRelativeRuleLine(rule),
    729          column: InspectorUtils.getRuleColumn(rule),
    730        });
    731      } else if (className === "CSSCustomMediaRule") {
    732        const customMediaQuery = [];
    733        if (typeof rule.query === "boolean") {
    734          customMediaQuery.push({
    735            text: rule.query.toString(),
    736            matches: rule.query === true,
    737          });
    738        } else {
    739          // if query is not a boolean, it's a MediaList
    740          for (let i = 0, len = rule.query.length; i < len; i++) {
    741            customMediaQuery.push({
    742              text: rule.query[i],
    743              // For now always consider the media query as matching.
    744              // This should be changed as part of Bug 2006379
    745              matches: true,
    746            });
    747          }
    748        }
    749        atRules.push({
    750          type: "custom-media",
    751          customMediaName: rule.name,
    752          customMediaQuery,
    753          line: InspectorUtils.getRelativeRuleLine(rule),
    754          column: InspectorUtils.getRuleColumn(rule),
    755        });
    756      }
    757    }
    758    return {
    759      ruleCount,
    760      atRules,
    761    };
    762  }
    763 
    764  /**
    765   * Called when the status of a media query support changes (i.e. it now matches, or it
    766   * was matching but isn't anymore)
    767   *
    768   * @param {string} resourceId
    769   *        The id associated with the stylesheet
    770   * @param {number} index
    771   *        The index of the media rule relatively to all the other at-rules of the stylesheet
    772   * @param {MediaQueryList} mql
    773   *        The result of matchMedia for the given media rule
    774   */
    775  #onMatchesChange(resourceId, index, mql) {
    776    this.#onStyleSheetUpdated({
    777      resourceId,
    778      updateKind: "matches-change",
    779      updates: {
    780        nestedResourceUpdates: [
    781          {
    782            path: ["atRules", index, "matches"],
    783            value: mql.matches,
    784          },
    785        ],
    786      },
    787    });
    788  }
    789 
    790  /**
    791   * Get the node href of a given stylesheet
    792   *
    793   * @param {StyleSheet} styleSheet
    794   * @returns {string}
    795   */
    796  getNodeHref(styleSheet) {
    797    const { ownerNode } = styleSheet;
    798    if (!ownerNode) {
    799      return null;
    800    }
    801 
    802    if (ownerNode.nodeType == ownerNode.DOCUMENT_NODE) {
    803      return ownerNode.location.href;
    804    }
    805 
    806    if (ownerNode.ownerDocument?.location) {
    807      return ownerNode.ownerDocument.location.href;
    808    }
    809 
    810    return null;
    811  }
    812 
    813  /**
    814   * Get the sourcemap base url of a given stylesheet
    815   *
    816   * @param {StyleSheet} styleSheet
    817   * @returns {string}
    818   */
    819  getSourcemapBaseURL(styleSheet) {
    820    // When the style is injected via nsIDOMWindowUtils.loadSheet, even
    821    // the parent style sheet has no owner, so default back to target actor
    822    // document
    823    const ownerNode = getStyleSheetOwnerNode(styleSheet);
    824    const ownerDocument = ownerNode
    825      ? ownerNode.ownerDocument
    826      : this.#targetActor.window;
    827 
    828    return getSourcemapBaseURL(
    829      // Technically resolveSourceURL should be used here alongside
    830      // "this.rawSheet.sourceURL", but the style inspector does not support
    831      // /*# sourceURL=*/ in CSS, so we're omitting it here (bug 880831).
    832      styleSheet.href || this.getNodeHref(styleSheet),
    833      ownerDocument
    834    );
    835  }
    836 
    837  /**
    838   * Get all the stylesheets for a given window
    839   *
    840   * @param {Window} window
    841   * @returns {Array<StyleSheet>}
    842   */
    843  async #getStyleSheetsForWindow(window) {
    844    const { document } = window;
    845    const documentOnly = !document.nodePrincipal.isSystemPrincipal;
    846 
    847    const styleSheets = [];
    848 
    849    for (const styleSheet of InspectorUtils.getAllStyleSheets(
    850      document,
    851      documentOnly
    852    )) {
    853      if (!this.#shouldListSheet(styleSheet)) {
    854        continue;
    855      }
    856 
    857      styleSheets.push(styleSheet);
    858 
    859      // Get all sheets, including imported ones
    860      const importedStyleSheets = await this.#getImportedStyleSheets(
    861        document,
    862        styleSheet
    863      );
    864      styleSheets.push(...importedStyleSheets);
    865    }
    866 
    867    return styleSheets;
    868  }
    869 
    870  /**
    871   * Returns true if a given stylesheet has an ancestor with the same url it has
    872   *
    873   * @param {StyleSheet} styleSheet
    874   * @returns {boolean}
    875   */
    876  #haveAncestorWithSameURL(styleSheet) {
    877    const href = styleSheet.href;
    878    while (styleSheet.parentStyleSheet) {
    879      if (styleSheet.parentStyleSheet.href == href) {
    880        return true;
    881      }
    882      styleSheet = styleSheet.parentStyleSheet;
    883    }
    884    return false;
    885  }
    886 
    887  /**
    888   * Helper function called when a property changed in a given stylesheet
    889   *
    890   * @param {string} resourceId
    891   *        The id of the stylesheet the change occured in
    892   * @param {string} property
    893   *        The property that was changed
    894   * @param {string} value
    895   *        The value of the property
    896   */
    897  #notifyPropertyChanged(resourceId, property, value) {
    898    this.#onStyleSheetUpdated({
    899      resourceId,
    900      updateKind: "property-change",
    901      updates: { resourceUpdates: { [property]: value } },
    902    });
    903  }
    904 
    905  /**
    906   * Event handler that is called when the state of applicable of style sheet is changed.
    907   *
    908   * For now, StyleSheetApplicableStateChanged event will be called at following timings.
    909   * - Append <link> of stylesheet to document
    910   * - Append <style> to document
    911   * - Change disable attribute of stylesheet object
    912   * - Change disable attribute of <link> to false
    913   * - Stylesheet is constructed.
    914   * When appending <link>, <style> or changing `disabled` attribute to false,
    915   * `applicable` is passed as true. The other hand, when changing `disabled`
    916   * to true, this will be false.
    917   *
    918   * NOTE: StyleSheetApplicableStateChanged is _not_ called when removing the <link>/<style>,
    919   *       but a StyleSheetRemovedEvent is emitted in such case (see #onStyleSheetRemoved)
    920   *
    921   * @param {StyleSheetApplicableStateChangedEvent}
    922   *        The triggering event.
    923   */
    924  #onApplicableStateChanged = ({ applicable, stylesheet: styleSheet }) => {
    925    if (
    926      // Have interest in applicable stylesheet only.
    927      applicable &&
    928      styleSheet.associatedDocument &&
    929      (!this.#targetActor.ignoreSubFrames ||
    930        styleSheet.associatedDocument.ownerGlobal ===
    931          this.#targetActor.window) &&
    932      this.#shouldListSheet(styleSheet) &&
    933      !this.#haveAncestorWithSameURL(styleSheet)
    934    ) {
    935      this.#registerStyleSheet(styleSheet);
    936    }
    937  };
    938 
    939  /**
    940   * Event handler that is called when a style sheet is removed.
    941   *
    942   * @param {StyleSheetRemovedEvent}
    943   *        The triggering event.
    944   */
    945  #onStylesheetRemoved = event => {
    946    this.#unregisterStyleSheet(event.stylesheet);
    947  };
    948 
    949  /**
    950   * If the stylesheet isn't registered yet, this function will generate an associated
    951   * resourceId and call registered `onAvailable` listeners.
    952   *
    953   * @param {StyleSheet} styleSheet
    954   * @returns {string} the associated resourceId
    955   */
    956  #registerStyleSheet(styleSheet) {
    957    const existingResourceId = this.#findStyleSheetResourceId(styleSheet);
    958    // If the stylesheet is already registered, there's no need to notify about it again.
    959    if (existingResourceId) {
    960      return existingResourceId;
    961    }
    962 
    963    // It's important to prefix the resourceId with the target actorID so we can't have
    964    // duplicated resource ids when the client connects to multiple targets.
    965    const resourceId = `${this.#targetActor.actorID}:stylesheet:${this
    966      .#styleSheetCount++}`;
    967    this.#styleSheetMap.set(resourceId, styleSheet);
    968 
    969    const creationData = this.#styleSheetCreationData?.get(styleSheet);
    970    this.#styleSheetCreationData?.delete(styleSheet);
    971 
    972    const onAvailablePromises = [];
    973    for (const onAvailable of this.#watchListeners.onAvailable) {
    974      onAvailablePromises.push(
    975        onAvailable({
    976          resourceId,
    977          styleSheet,
    978          creationData,
    979        })
    980      );
    981    }
    982 
    983    // creationData exists if this stylesheet was created via `addStyleSheet`.
    984    if (creationData) {
    985      //  We resolve the promise once the watcher sent the resources to the client,
    986      // so `addStyleSheet` calls can be fullfilled.
    987      Promise.all(onAvailablePromises).then(() => creationData?.resolve());
    988    }
    989    return resourceId;
    990  }
    991 
    992  /**
    993   * If the stylesheet is registered, this function will call registered `onDestroyed`
    994   * listeners with the stylesheet resourceId.
    995   *
    996   * @param {StyleSheet} styleSheet
    997   */
    998  #unregisterStyleSheet(styleSheet) {
    999    const existingResourceId = this.#findStyleSheetResourceId(styleSheet);
   1000    if (!existingResourceId) {
   1001      return;
   1002    }
   1003 
   1004    this.#styleSheetMap.delete(existingResourceId);
   1005    this.#styleSheetCreationData?.delete(styleSheet);
   1006    if (this.#mqlChangeAbortControllerMap.has(existingResourceId)) {
   1007      this.#mqlChangeAbortControllerMap.get(existingResourceId).abort();
   1008      this.#mqlChangeAbortControllerMap.delete(existingResourceId);
   1009    }
   1010 
   1011    for (const onDestroyed of this.#watchListeners.onDestroyed) {
   1012      onDestroyed({
   1013        resourceId: existingResourceId,
   1014      });
   1015    }
   1016  }
   1017 
   1018  #onStyleSheetUpdated(data) {
   1019    this.emit("stylesheet-updated", data);
   1020 
   1021    for (const onUpdated of this.#watchListeners.onUpdated) {
   1022      onUpdated(data);
   1023    }
   1024  }
   1025 
   1026  /**
   1027   * Returns true if the passed styleSheet should be handled.
   1028   *
   1029   * @param {StyleSheet} styleSheet
   1030   * @returns {boolean}
   1031   */
   1032  #shouldListSheet(styleSheet) {
   1033    const href = styleSheet.href?.toLowerCase();
   1034    // FIXME(bug 1826538): Make accessiblecaret.css and similar UA-widget
   1035    // sheets system sheets, then remove this special-case.
   1036    if (
   1037      href === "resource://gre-resources/accessiblecaret.css" ||
   1038      href === "resource://gre-resources/details.css" ||
   1039      (href === "resource://devtools-highlighter-styles/highlighters.css" &&
   1040        this.#targetActor.sessionContext.type !== "all")
   1041    ) {
   1042      return false;
   1043    }
   1044    return true;
   1045  }
   1046 
   1047  /**
   1048   * The StyleSheetsManager instance is managed by the target, so this will be called when
   1049   * the target gets destroyed.
   1050   */
   1051  destroy() {
   1052    // Cleanup
   1053    if (this.#abortController) {
   1054      this.#abortController.abort();
   1055    }
   1056    if (this.#mqlChangeAbortControllerMap) {
   1057      for (const ac of this.#mqlChangeAbortControllerMap.values()) {
   1058        ac.abort();
   1059      }
   1060    }
   1061 
   1062    try {
   1063      this.#unwatchStyleSheetChangeEvents();
   1064    } catch (e) {
   1065      console.error(
   1066        "Error when destroying StyleSheet manager for",
   1067        this.#targetActor,
   1068        ": ",
   1069        e
   1070      );
   1071    }
   1072 
   1073    this.#styleSheetMap.clear();
   1074    this.#abortController = null;
   1075    this.#mqlChangeAbortControllerMap = null;
   1076    this.#styleSheetCreationData = null;
   1077    this.#styleSheetMap = null;
   1078    this.#targetActor = null;
   1079    this.#watchListeners = null;
   1080  }
   1081 }
   1082 
   1083 module.exports = {
   1084  StyleSheetsManager,
   1085  UPDATE_GENERAL,
   1086  UPDATE_PRESERVING_RULES,
   1087 };