tor-browser

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

rule.js (28395B)


      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 {
      8  style: { ELEMENT_STYLE, PRES_HINTS },
      9 } = require("resource://devtools/shared/constants.js");
     10 const CssLogic = require("resource://devtools/shared/inspector/css-logic.js");
     11 const TextProperty = require("resource://devtools/client/inspector/rules/models/text-property.js");
     12 
     13 loader.lazyRequireGetter(
     14  this,
     15  "promiseWarn",
     16  "resource://devtools/client/inspector/shared/utils.js",
     17  true
     18 );
     19 loader.lazyRequireGetter(
     20  this,
     21  "parseNamedDeclarations",
     22  "resource://devtools/shared/css/parsing-utils.js",
     23  true
     24 );
     25 
     26 const STYLE_INSPECTOR_PROPERTIES =
     27  "devtools/shared/locales/styleinspector.properties";
     28 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
     29 const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES);
     30 
     31 /**
     32 * Rule is responsible for the following:
     33 *   Manages a single style declaration or rule.
     34 *   Applies changes to the properties in a rule.
     35 *   Maintains a list of TextProperty objects.
     36 */
     37 class Rule {
     38  /**
     39   * @param {ElementStyle} elementStyle
     40   *        The ElementStyle to which this rule belongs.
     41   * @param {object} options
     42   *        The information used to construct this rule. Properties include:
     43   *          rule: A StyleRuleActor
     44   *          inherited: An element this rule was inherited from. If omitted,
     45   *            the rule applies directly to the current element.
     46   *          isSystem: Is this a user agent style?
     47   *          isUnmatched: True if the rule does not match the current selected
     48   *            element, otherwise, false.
     49   */
     50  constructor(elementStyle, options) {
     51    this.elementStyle = elementStyle;
     52    this.domRule = options.rule;
     53    this.compatibilityIssues = null;
     54 
     55    this.matchedSelectorIndexes = options.matchedSelectorIndexes || [];
     56    this.isSystem = options.isSystem;
     57    this.isUnmatched = options.isUnmatched || false;
     58    this.darkColorScheme = options.darkColorScheme;
     59    this.inherited = options.inherited || null;
     60    this.pseudoElement = options.pseudoElement || "";
     61    this.keyframes = options.keyframes || null;
     62    this.userAdded = options.rule.userAdded;
     63 
     64    this.cssProperties = this.elementStyle.ruleView.cssProperties;
     65    this.inspector = this.elementStyle.ruleView.inspector;
     66    this.store = this.elementStyle.ruleView.store;
     67 
     68    // Populate the text properties with the style's current authoredText
     69    // value, and add in any disabled properties from the store.
     70    this.textProps = this._getTextProperties();
     71    this.textProps = this.textProps.concat(this._getDisabledProperties());
     72 
     73    this.getUniqueSelector = this.getUniqueSelector.bind(this);
     74    this.onStyleRuleFrontUpdated = this.onStyleRuleFrontUpdated.bind(this);
     75 
     76    this.domRule.on("rule-updated", this.onStyleRuleFrontUpdated);
     77  }
     78 
     79  destroy() {
     80    if (this._unsubscribeSourceMap) {
     81      this._unsubscribeSourceMap();
     82    }
     83 
     84    this.domRule.off("rule-updated", this.onStyleRuleFrontUpdated);
     85    this.compatibilityIssues = null;
     86    this.destroyed = true;
     87  }
     88 
     89  get declarations() {
     90    return this.textProps;
     91  }
     92 
     93  get selector() {
     94    return {
     95      getUniqueSelector: this.getUniqueSelector,
     96      matchedSelectorIndexes: this.matchedSelectorIndexes,
     97      selectors: this.domRule.selectors,
     98      selectorsSpecificity: this.domRule.selectorsSpecificity,
     99      selectorWarnings: this.domRule.selectors,
    100      selectorText: this.keyframes ? this.domRule.keyText : this.selectorText,
    101    };
    102  }
    103 
    104  get sourceMapURLService() {
    105    return this.inspector.toolbox.sourceMapURLService;
    106  }
    107 
    108  get title() {
    109    let title = CssLogic.shortSource(this.sheet);
    110    if (this.domRule.type !== ELEMENT_STYLE && this.ruleLine > 0) {
    111      title += ":" + this.ruleLine;
    112    }
    113 
    114    return title;
    115  }
    116 
    117  get inheritedSectionLabel() {
    118    if (this._inheritedSectionLabel) {
    119      return this._inheritedSectionLabel;
    120    }
    121    this._inheritedSectionLabel = "";
    122    if (this.inherited) {
    123      let eltText = this.inherited.displayName;
    124      if (this.inherited.id) {
    125        eltText += "#" + this.inherited.id;
    126      }
    127      if (CssLogic.ELEMENT_BACKED_PSEUDO_ELEMENTS.has(this.pseudoElement)) {
    128        eltText += this.pseudoElement;
    129      }
    130      this._inheritedSectionLabel = STYLE_INSPECTOR_L10N.getFormatStr(
    131        "rule.inheritedFrom",
    132        eltText
    133      );
    134    }
    135    return this._inheritedSectionLabel;
    136  }
    137 
    138  get keyframesName() {
    139    if (this._keyframesName) {
    140      return this._keyframesName;
    141    }
    142    this._keyframesName = "";
    143    if (this.keyframes) {
    144      this._keyframesName = STYLE_INSPECTOR_L10N.getFormatStr(
    145        "rule.keyframe",
    146        this.keyframes.name
    147      );
    148    }
    149    return this._keyframesName;
    150  }
    151 
    152  get keyframesRule() {
    153    if (!this.keyframes) {
    154      return null;
    155    }
    156 
    157    return {
    158      id: this.keyframes.actorID,
    159      keyframesName: this.keyframesName,
    160    };
    161  }
    162 
    163  get selectorText() {
    164    if (Array.isArray(this.domRule.selectors)) {
    165      return this.domRule.selectors.join(", ");
    166    }
    167 
    168    if (this.domRule.type === PRES_HINTS) {
    169      return CssLogic.l10n("rule.sourceElementAttributesStyle");
    170    }
    171 
    172    return CssLogic.l10n("rule.sourceElement");
    173  }
    174 
    175  /**
    176   * The rule's stylesheet.
    177   */
    178  get sheet() {
    179    return this.domRule ? this.domRule.parentStyleSheet : null;
    180  }
    181 
    182  /**
    183   * The rule's line within a stylesheet
    184   */
    185  get ruleLine() {
    186    return this.domRule ? this.domRule.line : -1;
    187  }
    188 
    189  /**
    190   * The rule's column within a stylesheet
    191   */
    192  get ruleColumn() {
    193    return this.domRule ? this.domRule.column : null;
    194  }
    195 
    196  /**
    197   * Get the declaration block issues from the compatibility actor
    198   *
    199   * @returns A promise that resolves with an array of objects in following form:
    200   *    {
    201   *      // Type of compatibility issue
    202   *      type: <string>,
    203   *      // The CSS declaration that has compatibility issues
    204   *      property: <string>,
    205   *      // Alias to the given CSS property
    206   *      alias: <Array>,
    207   *      // Link to MDN documentation for the particular CSS rule
    208   *      url: <string>,
    209   *      deprecated: <boolean>,
    210   *      experimental: <boolean>,
    211   *      // An array of all the browsers that don't support the given CSS rule
    212   *      unsupportedBrowsers: <Array>,
    213   *    }
    214   */
    215  async getCompatibilityIssues() {
    216    if (!this.compatibilityIssues) {
    217      this.compatibilityIssues =
    218        this.inspector.commands.inspectorCommand.getCSSDeclarationBlockIssues(
    219          this.domRule.declarations
    220        );
    221    }
    222 
    223    return this.compatibilityIssues;
    224  }
    225 
    226  /**
    227   * Returns the TextProperty with the given id or undefined if it cannot be found.
    228   *
    229   * @param {string | null} id
    230   *        A TextProperty id.
    231   * @return {TextProperty|undefined} with the given id in the current Rule or undefined
    232   * if it cannot be found.
    233   */
    234  getDeclaration(id) {
    235    return id ? this.textProps.find(textProp => textProp.id === id) : undefined;
    236  }
    237 
    238  /**
    239   * Returns an unique selector for the CSS rule.
    240   */
    241  async getUniqueSelector() {
    242    let selector = "";
    243 
    244    if (this.domRule.selectors) {
    245      // This is a style rule with a selector.
    246      selector = this.domRule.selectors.join(", ");
    247    } else if (this.inherited) {
    248      // This is an inline style from an inherited rule. Need to resolve the unique
    249      // selector from the node which rule this is inherited from.
    250      selector = await this.inherited.getUniqueSelector();
    251    } else {
    252      // This is an inline style from the current node.
    253      selector = await this.inspector.selection.nodeFront.getUniqueSelector();
    254    }
    255 
    256    return selector;
    257  }
    258 
    259  /**
    260   * Returns true if the rule matches the creation options
    261   * specified.
    262   *
    263   * @param {object} options
    264   *        Creation options. See the Rule constructor for documentation.
    265   */
    266  matches(options) {
    267    return this.domRule === options.rule;
    268  }
    269 
    270  /**
    271   * Create a new TextProperty to include in the rule.
    272   *
    273   * @param {string} name
    274   *        The text property name (such as "background" or "border-top").
    275   * @param {string} value
    276   *        The property's value (not including priority).
    277   * @param {string} priority
    278   *        The property's priority (either "important" or an empty string).
    279   * @param {boolean} enabled
    280   *        True if the property should be enabled.
    281   * @param {TextProperty} siblingProp
    282   *        Optional, property next to which the new property will be added.
    283   */
    284  createProperty(name, value, priority, enabled, siblingProp) {
    285    const prop = new TextProperty({
    286      rule: this,
    287      name,
    288      value,
    289      priority,
    290      enabled,
    291    });
    292 
    293    let ind;
    294    if (siblingProp) {
    295      ind = this.textProps.indexOf(siblingProp) + 1;
    296      this.textProps.splice(ind, 0, prop);
    297    } else {
    298      ind = this.textProps.length;
    299      this.textProps.push(prop);
    300    }
    301 
    302    this.applyProperties(modifications => {
    303      modifications.createProperty(ind, name, value, priority, enabled);
    304 
    305      this.store.userProperties.setProperty(this.domRule, name, value);
    306 
    307      // Now that the rule has been updated, the server might have given us data
    308      // that changes the state of the property. Update it now.
    309      prop.updateEditor();
    310    });
    311 
    312    return prop;
    313  }
    314 
    315  /**
    316   * Helper function for applyProperties that is called when the actor
    317   * does not support as-authored styles.  Store disabled properties
    318   * in the element style's store.
    319   */
    320  _applyPropertiesNoAuthored(modifications) {
    321    this.elementStyle.onRuleUpdated();
    322 
    323    const disabledProps = [];
    324 
    325    for (const prop of this.textProps) {
    326      if (prop.invisible) {
    327        continue;
    328      }
    329      if (!prop.enabled) {
    330        disabledProps.push({
    331          name: prop.name,
    332          value: prop.value,
    333          priority: prop.priority,
    334        });
    335        continue;
    336      }
    337      if (prop.value.trim() === "") {
    338        continue;
    339      }
    340 
    341      modifications.setProperty(-1, prop.name, prop.value, prop.priority);
    342 
    343      prop.updateComputed();
    344    }
    345 
    346    // Store disabled properties in the disabled store.
    347    const disabled = this.elementStyle.store.disabled;
    348    if (disabledProps.length) {
    349      disabled.set(this.domRule, disabledProps);
    350    } else {
    351      disabled.delete(this.domRule);
    352    }
    353 
    354    return modifications.apply().then(() => {
    355      const cssProps = {};
    356      // Note that even though StyleRuleActors normally provide parsed
    357      // declarations already, _applyPropertiesNoAuthored is only used when
    358      // connected to older backend that do not provide them. So parse here.
    359      for (const cssProp of parseNamedDeclarations(
    360        this.cssProperties.isKnown,
    361        this.domRule.authoredText
    362      )) {
    363        cssProps[cssProp.name] = cssProp;
    364      }
    365 
    366      for (const textProp of this.textProps) {
    367        if (!textProp.enabled) {
    368          continue;
    369        }
    370        let cssProp = cssProps[textProp.name];
    371 
    372        if (!cssProp) {
    373          cssProp = {
    374            name: textProp.name,
    375            value: "",
    376            priority: "",
    377          };
    378        }
    379 
    380        textProp.priority = cssProp.priority;
    381      }
    382    });
    383  }
    384 
    385  /**
    386   * A helper for applyProperties that applies properties in the "as
    387   * authored" case; that is, when the StyleRuleActor supports
    388   * setRuleText.
    389   */
    390  _applyPropertiesAuthored(modifications) {
    391    return modifications.apply().then(() => {
    392      // The rewriting may have required some other property values to
    393      // change, e.g., to insert some needed terminators.  Update the
    394      // relevant properties here.
    395      for (const index in modifications.changedDeclarations) {
    396        const newValue = modifications.changedDeclarations[index];
    397        this.textProps[index].updateValue(newValue);
    398      }
    399      // Recompute and redisplay the computed properties.
    400      for (const prop of this.textProps) {
    401        if (!prop.invisible && prop.enabled) {
    402          prop.updateComputed();
    403          prop.updateEditor();
    404        }
    405      }
    406    });
    407  }
    408 
    409  /**
    410   * Reapply all the properties in this rule, and update their
    411   * computed styles.  Will re-mark overridden properties.  Sets the
    412   * |_applyingModifications| property to a promise which will resolve
    413   * when the edit has completed.
    414   *
    415   * @param {Function} modifier a function that takes a RuleModificationList
    416   *        (or RuleRewriter) as an argument and that modifies it
    417   *        to apply the desired edit
    418   * @return {Promise} a promise which will resolve when the edit
    419   *        is complete
    420   */
    421  applyProperties(modifier) {
    422    // If there is already a pending modification, we have to wait
    423    // until it settles before applying the next modification.
    424    const resultPromise = Promise.resolve(this._applyingModifications)
    425      .then(() => {
    426        const modifications = this.domRule.startModifyingProperties(
    427          this.inspector.panelWin,
    428          this.cssProperties
    429        );
    430        modifier(modifications);
    431        if (this.domRule.canSetRuleText) {
    432          return this._applyPropertiesAuthored(modifications);
    433        }
    434        return this._applyPropertiesNoAuthored(modifications);
    435      })
    436      .then(() => {
    437        this.elementStyle.onRuleUpdated();
    438 
    439        if (resultPromise === this._applyingModifications) {
    440          this._applyingModifications = null;
    441          this.elementStyle._changed();
    442        }
    443      })
    444      .catch(promiseWarn);
    445 
    446    this._applyingModifications = resultPromise;
    447    return resultPromise;
    448  }
    449 
    450  /**
    451   * Renames a property.
    452   *
    453   * @param {TextProperty} property
    454   *        The property to rename.
    455   * @param {string} name
    456   *        The new property name (such as "background" or "border-top").
    457   * @return {Promise}
    458   */
    459  setPropertyName(property, name) {
    460    if (name === property.name) {
    461      return Promise.resolve();
    462    }
    463 
    464    const oldName = property.name;
    465    property.name = name;
    466    const index = this.textProps.indexOf(property);
    467    return this.applyProperties(modifications => {
    468      modifications.renameProperty(index, oldName, name);
    469    });
    470  }
    471 
    472  /**
    473   * Sets the value and priority of a property, then reapply all properties.
    474   *
    475   * @param {TextProperty} property
    476   *        The property to manipulate.
    477   * @param {string} value
    478   *        The property's value (not including priority).
    479   * @param {string} priority
    480   *        The property's priority (either "important" or an empty string).
    481   * @return {Promise}
    482   */
    483  setPropertyValue(property, value, priority) {
    484    if (value === property.value && priority === property.priority) {
    485      return Promise.resolve();
    486    }
    487 
    488    property.value = value;
    489    property.priority = priority;
    490 
    491    const index = this.textProps.indexOf(property);
    492    return this.applyProperties(modifications => {
    493      modifications.setProperty(index, property.name, value, priority);
    494    });
    495  }
    496 
    497  /**
    498   * Just sets the value and priority of a property, in order to preview its
    499   * effect on the content document.
    500   *
    501   * @param {TextProperty} property
    502   *        The property which value will be previewed
    503   * @param {string} value
    504   *        The value to be used for the preview
    505   * @param {string} priority
    506   *        The property's priority (either "important" or an empty string).
    507   * @return {Promise}
    508   */
    509  previewPropertyValue(property, value, priority) {
    510    this.elementStyle.ruleView.emitForTests("start-preview-property-value");
    511    const modifications = this.domRule.startModifyingProperties(
    512      this.inspector.panelWin,
    513      this.cssProperties
    514    );
    515    modifications.setProperty(
    516      this.textProps.indexOf(property),
    517      property.name,
    518      value,
    519      priority
    520    );
    521    return modifications.apply().then(() => {
    522      // Ensure dispatching a ruleview-changed event
    523      // also for previews
    524      this.elementStyle._changed();
    525    });
    526  }
    527 
    528  /**
    529   * Disables or enables given TextProperty.
    530   *
    531   * @param {TextProperty} property
    532   *        The property to enable/disable
    533   * @param {boolean} value
    534   */
    535  setPropertyEnabled(property, value) {
    536    if (property.enabled === !!value) {
    537      return;
    538    }
    539    property.enabled = !!value;
    540    const index = this.textProps.indexOf(property);
    541    this.applyProperties(modifications => {
    542      modifications.setPropertyEnabled(index, property.name, property.enabled);
    543    });
    544  }
    545 
    546  /**
    547   * Remove a given TextProperty from the rule and update the rule
    548   * accordingly.
    549   *
    550   * @param {TextProperty} property
    551   *        The property to be removed
    552   */
    553  removeProperty(property) {
    554    const index = this.textProps.indexOf(property);
    555    this.textProps.splice(index, 1);
    556    // Need to re-apply properties in case removing this TextProperty
    557    // exposes another one.
    558    this.applyProperties(modifications => {
    559      modifications.removeProperty(index, property.name);
    560    });
    561  }
    562 
    563  /**
    564   * Event handler for "rule-updated" event fired by StyleRuleActor.
    565   *
    566   * @param {StyleRuleFront} front
    567   */
    568  onStyleRuleFrontUpdated(front) {
    569    // Overwritting this reference is not required, but it's here to avoid confusion.
    570    // Whenever an actor is passed over the protocol, either as a return value or as
    571    // payload on an event, the `form` of its corresponding front will be automatically
    572    // updated. No action required.
    573    // Even if this `domRule` reference here is not explicitly updated, lookups of
    574    // `this.domRule.declarations` will point to the latest state of declarations set
    575    // on the actor. Everything on `StyleRuleForm.form` will point to the latest state.
    576    this.domRule = front;
    577  }
    578 
    579  /**
    580   * Get the list of TextProperties from the style. Needs
    581   * to parse the style's authoredText.
    582   */
    583  _getTextProperties() {
    584    const textProps = [];
    585    const store = this.elementStyle.store;
    586 
    587    for (const prop of this.domRule.declarations) {
    588      const name = prop.name;
    589      // In an inherited rule, we only show inherited properties.
    590      // However, we must keep all properties in order for rule
    591      // rewriting to work properly.  So, compute the "invisible"
    592      // property here.
    593      const inherits = prop.isCustomProperty
    594        ? prop.inherits
    595        : this.cssProperties.isInherited(name);
    596      const invisible = this.inherited && !inherits;
    597 
    598      const value = store.userProperties.getProperty(
    599        this.domRule,
    600        name,
    601        prop.value
    602      );
    603 
    604      const textProp = new TextProperty({
    605        rule: this,
    606        name,
    607        value,
    608        priority: prop.priority,
    609        enabled: !("commentOffsets" in prop),
    610        invisible,
    611      });
    612      textProps.push(textProp);
    613    }
    614 
    615    return textProps;
    616  }
    617 
    618  /**
    619   * Return the list of disabled properties from the store for this rule.
    620   */
    621  _getDisabledProperties() {
    622    const store = this.elementStyle.store;
    623 
    624    // Include properties from the disabled property store, if any.
    625    const disabledProps = store.disabled.get(this.domRule);
    626    if (!disabledProps) {
    627      return [];
    628    }
    629 
    630    const textProps = [];
    631 
    632    for (const prop of disabledProps) {
    633      const value = store.userProperties.getProperty(
    634        this.domRule,
    635        prop.name,
    636        prop.value
    637      );
    638      const textProp = new TextProperty({
    639        rule: this,
    640        name: prop.name,
    641        value,
    642        priority: prop.priority,
    643      });
    644      textProp.enabled = false;
    645      textProps.push(textProp);
    646    }
    647 
    648    return textProps;
    649  }
    650 
    651  /**
    652   * Reread the current state of the rules and rebuild text
    653   * properties as needed.
    654   */
    655  refresh(options) {
    656    this.matchedSelectorIndexes = options.matchedSelectorIndexes || [];
    657    const colorSchemeChanged = this.darkColorScheme !== options.darkColorScheme;
    658    this.darkColorScheme = options.darkColorScheme;
    659 
    660    const newTextProps = this._getTextProperties();
    661 
    662    // The element style rule behaves differently on refresh. We basically need to update
    663    // it to reflect the new text properties exactly. The order might have changed, some
    664    // properties might have been removed, etc. And we don't need to mark anything as
    665    // disabled here. The element style rule should always reflect the content of the
    666    // style attribute.
    667    if (this.domRule.type === ELEMENT_STYLE) {
    668      this.textProps = newTextProps;
    669 
    670      if (this.editor) {
    671        this.editor.populate(true);
    672      }
    673 
    674      return;
    675    }
    676 
    677    // Update current properties for each property present on the style.
    678    // This will mark any touched properties with _visited so we
    679    // can detect properties that weren't touched (because they were
    680    // removed from the style).
    681    // Also keep track of properties that didn't exist in the current set
    682    // of properties.
    683    const brandNewProps = [];
    684    for (const newProp of newTextProps) {
    685      if (!this._updateTextProperty(newProp)) {
    686        brandNewProps.push(newProp);
    687      }
    688    }
    689 
    690    // Refresh editors and disabled state for all the properties that
    691    // were updated.
    692    for (const prop of this.textProps) {
    693      // Properties that weren't touched during the update
    694      // process must no longer exist on the node.  Mark them disabled.
    695      if (!prop._visited) {
    696        prop.enabled = false;
    697        prop.updateEditor();
    698      } else {
    699        delete prop._visited;
    700      }
    701 
    702      // Valid properties that aren't disabled might need to get updated in some condition
    703      if (
    704        prop.enabled &&
    705        prop.isValid() &&
    706        // Update if it's using light-dark and the color scheme changed
    707        colorSchemeChanged &&
    708        prop.value.includes("light-dark")
    709      ) {
    710        prop.updateEditor();
    711      }
    712    }
    713 
    714    // Add brand new properties.
    715    this.textProps = this.textProps.concat(brandNewProps);
    716 
    717    // Refresh the editor if one already exists.
    718    if (this.editor) {
    719      this.editor.populate();
    720    }
    721  }
    722 
    723  /**
    724   * Update the current TextProperties that match a given property
    725   * from the authoredText.  Will choose one existing TextProperty to update
    726   * with the new property's value, and will disable all others.
    727   *
    728   * When choosing the best match to reuse, properties will be chosen
    729   * by assigning a rank and choosing the highest-ranked property:
    730   *   Name, value, and priority match, enabled. (6)
    731   *   Name, value, and priority match, disabled. (5)
    732   *   Name and value match, enabled. (4)
    733   *   Name and value match, disabled. (3)
    734   *   Name matches, enabled. (2)
    735   *   Name matches, disabled. (1)
    736   *
    737   * If no existing properties match the property, nothing happens.
    738   *
    739   * @param {TextProperty} newProp
    740   *        The current version of the property, as parsed from the
    741   *        authoredText in Rule._getTextProperties().
    742   * @return {boolean} true if a property was updated, false if no properties
    743   *         were updated.
    744   */
    745  _updateTextProperty(newProp) {
    746    const match = { rank: 0, prop: null };
    747 
    748    for (const prop of this.textProps) {
    749      if (prop.name !== newProp.name) {
    750        continue;
    751      }
    752 
    753      // Mark this property visited.
    754      prop._visited = true;
    755 
    756      // Start at rank 1 for matching name.
    757      let rank = 1;
    758 
    759      // Value and Priority matches add 2 to the rank.
    760      // Being enabled adds 1.  This ranks better matches higher,
    761      // with priority breaking ties.
    762      if (prop.value === newProp.value) {
    763        rank += 2;
    764        if (prop.priority === newProp.priority) {
    765          rank += 2;
    766        }
    767      }
    768 
    769      if (prop.enabled) {
    770        rank += 1;
    771      }
    772 
    773      if (rank > match.rank) {
    774        if (match.prop) {
    775          // We outrank a previous match, disable it.
    776          match.prop.enabled = false;
    777          match.prop.updateEditor();
    778        }
    779        match.rank = rank;
    780        match.prop = prop;
    781      } else if (rank) {
    782        // A previous match outranks us, disable ourself.
    783        prop.enabled = false;
    784        prop.updateEditor();
    785      }
    786    }
    787 
    788    // If we found a match, update its value with the new text property
    789    // value.
    790    if (match.prop) {
    791      match.prop.set(newProp);
    792      return true;
    793    }
    794 
    795    return false;
    796  }
    797 
    798  /**
    799   * Jump between editable properties in the UI. If the focus direction is
    800   * forward, begin editing the next property name if available or focus the
    801   * new property editor otherwise. If the focus direction is backward,
    802   * begin editing the previous property value or focus the selector editor if
    803   * this is the first element in the property list.
    804   *
    805   * @param {TextProperty} textProperty
    806   *        The text property that will be left to focus on a sibling.
    807   * @param {number} direction
    808   *        The move focus direction number.
    809   */
    810  editClosestTextProperty(textProperty, direction) {
    811    let index = this.textProps.indexOf(textProperty);
    812 
    813    if (direction === Services.focus.MOVEFOCUS_FORWARD) {
    814      for (++index; index < this.textProps.length; ++index) {
    815        // The prop could be invisible or a hidden unused variable
    816        if (this.textProps[index].editor) {
    817          break;
    818        }
    819      }
    820      if (index === this.textProps.length) {
    821        textProperty.rule.editor.closeBrace.click();
    822      } else {
    823        this.textProps[index].editor.nameSpan.click();
    824      }
    825    } else if (direction === Services.focus.MOVEFOCUS_BACKWARD) {
    826      for (--index; index >= 0; --index) {
    827        // The prop could be invisible or a hidden unused variable
    828        if (this.textProps[index].editor) {
    829          break;
    830        }
    831      }
    832      if (index < 0) {
    833        textProperty.editor.ruleEditor.selectorText.click();
    834      } else {
    835        this.textProps[index].editor.valueSpan.click();
    836      }
    837    }
    838  }
    839 
    840  /**
    841   * Return a string representation of the rule.
    842   */
    843  stringifyRule() {
    844    const selectorText = this.selectorText;
    845    let cssText = "";
    846    const terminator = Services.appinfo.OS === "WINNT" ? "\r\n" : "\n";
    847 
    848    for (const textProp of this.textProps) {
    849      if (!textProp.invisible) {
    850        cssText += "\t" + textProp.stringifyProperty() + terminator;
    851      }
    852    }
    853 
    854    return selectorText + " {" + terminator + cssText + "}";
    855  }
    856 
    857  /**
    858   * @returns {boolean} Whether or not the rule is in a layer
    859   */
    860  isInLayer() {
    861    return this.domRule.ancestorData.some(({ type }) => type === "layer");
    862  }
    863 
    864  /**
    865   * Return whether this rule and the one passed are in the same layer,
    866   * (as in described in the spec; this is not checking that the 2 rules are children
    867   * of the same CSSLayerBlockRule)
    868   *
    869   * @param {Rule} otherRule: The rule we want to compare with
    870   * @returns {boolean}
    871   */
    872  isInDifferentLayer(otherRule) {
    873    const filterLayer = ({ type }) => type === "layer";
    874    const thisLayers = this.domRule.ancestorData.filter(filterLayer);
    875    const otherRuleLayers = otherRule.domRule.ancestorData.filter(filterLayer);
    876 
    877    if (thisLayers.length !== otherRuleLayers.length) {
    878      return true;
    879    }
    880 
    881    return thisLayers.some((layer, i) => {
    882      const otherRuleLayer = otherRuleLayers[i];
    883      // For named layers, we can compare the layer name directly, since we want to identify
    884      // the actual layer, not the specific CSSLayerBlockRule.
    885      // For nameless layers though, we don't have a choice and we can only identify them
    886      // via their CSSLayerBlockRule, so we're using the rule actorID.
    887      return (
    888        (layer.value || layer.actorID) !==
    889        (otherRuleLayer.value || otherRuleLayer.actorID)
    890      );
    891    });
    892  }
    893 
    894  /**
    895   * @returns {boolean} Whether or not the rule is in a @starting-style rule
    896   */
    897  isInStartingStyle() {
    898    return this.domRule.ancestorData.some(
    899      ({ type }) => type === "starting-style"
    900    );
    901  }
    902 
    903  /**
    904   * @returns {boolean} Whether or not the rule can be edited
    905   */
    906  isEditable() {
    907    return (
    908      !this.isSystem &&
    909      this.domRule.type !== PRES_HINTS &&
    910      // FIXME: Should be removed as part of Bug 2004046
    911      this.domRule.className !== "CSSPositionTryRule"
    912    );
    913  }
    914 
    915  /**
    916   * See whether this rule has any non-invisible properties.
    917   *
    918   * @return {boolean} true if there is any visible property, or false
    919   *         if all properties are invisible
    920   */
    921  hasAnyVisibleProperties() {
    922    for (const prop of this.textProps) {
    923      if (!prop.invisible) {
    924        return true;
    925      }
    926    }
    927    return false;
    928  }
    929 }
    930 
    931 module.exports = Rule;