summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/inspector/css-logic.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/inspector/css-logic.js')
-rw-r--r--devtools/server/actors/inspector/css-logic.js1604
1 files changed, 1604 insertions, 0 deletions
diff --git a/devtools/server/actors/inspector/css-logic.js b/devtools/server/actors/inspector/css-logic.js
new file mode 100644
index 0000000000..8ef0978915
--- /dev/null
+++ b/devtools/server/actors/inspector/css-logic.js
@@ -0,0 +1,1604 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * About the objects defined in this file:
+ * - CssLogic contains style information about a view context. It provides
+ * access to 2 sets of objects: Css[Sheet|Rule|Selector] provide access to
+ * information that does not change when the selected element changes while
+ * Css[Property|Selector]Info provide information that is dependent on the
+ * selected element.
+ * Its key methods are highlight(), getPropertyInfo() and forEachSheet(), etc
+ *
+ * - CssSheet provides a more useful API to a DOM CSSSheet for our purposes,
+ * including shortSource and href.
+ * - CssRule a more useful API to a DOM CSSRule including access to the group
+ * of CssSelectors that the rule provides properties for
+ * - CssSelector A single selector - i.e. not a selector group. In other words
+ * a CssSelector does not contain ','. This terminology is different from the
+ * standard DOM API, but more inline with the definition in the spec.
+ *
+ * - CssPropertyInfo contains style information for a single property for the
+ * highlighted element.
+ * - CssSelectorInfo is a wrapper around CssSelector, which adds sorting with
+ * reference to the selected element.
+ */
+
+"use strict";
+
+const nodeConstants = require("resource://devtools/shared/dom-node-constants.js");
+const {
+ getBindingElementAndPseudo,
+ getCSSStyleRules,
+ hasVisitedState,
+ isAgentStylesheet,
+ isAuthorStylesheet,
+ isUserStylesheet,
+ shortSource,
+ FILTER,
+ STATUS,
+} = require("resource://devtools/shared/inspector/css-logic.js");
+
+const COMPAREMODE = {
+ BOOLEAN: "bool",
+ INTEGER: "int",
+};
+
+class CssLogic {
+ constructor() {
+ this._propertyInfos = {};
+ }
+
+ // Both setup by highlight().
+ viewedElement = null;
+ viewedDocument = null;
+
+ // The cache of the known sheets.
+ _sheets = null;
+
+ // Have the sheets been cached?
+ _sheetsCached = false;
+
+ // The total number of rules, in all stylesheets, after filtering.
+ _ruleCount = 0;
+
+ // The computed styles for the viewedElement.
+ _computedStyle = null;
+
+ // Source filter. Only display properties coming from the given source
+ _sourceFilter = FILTER.USER;
+
+ // Used for tracking unique CssSheet/CssRule/CssSelector objects, in a run of
+ // processMatchedSelectors().
+ _passId = 0;
+
+ // Used for tracking matched CssSelector objects.
+ _matchId = 0;
+
+ _matchedRules = null;
+ _matchedSelectors = null;
+
+ // Cached keyframes rules in all stylesheets
+ _keyframesRules = null;
+
+ /**
+ * Reset various properties
+ */
+ reset() {
+ this._propertyInfos = {};
+ this._ruleCount = 0;
+ this._sheetIndex = 0;
+ this._sheets = {};
+ this._sheetsCached = false;
+ this._matchedRules = null;
+ this._matchedSelectors = null;
+ this._keyframesRules = [];
+ }
+
+ /**
+ * Focus on a new element - remove the style caches.
+ *
+ * @param {Element} aViewedElement the element the user has highlighted
+ * in the Inspector.
+ */
+ highlight(viewedElement) {
+ if (!viewedElement) {
+ this.viewedElement = null;
+ this.viewedDocument = null;
+ this._computedStyle = null;
+ this.reset();
+ return;
+ }
+
+ if (viewedElement === this.viewedElement) {
+ return;
+ }
+
+ this.viewedElement = viewedElement;
+
+ const doc = this.viewedElement.ownerDocument;
+ if (doc != this.viewedDocument) {
+ // New document: clear/rebuild the cache.
+ this.viewedDocument = doc;
+
+ // Hunt down top level stylesheets, and cache them.
+ this._cacheSheets();
+ } else {
+ // Clear cached data in the CssPropertyInfo objects.
+ this._propertyInfos = {};
+ }
+
+ this._matchedRules = null;
+ this._matchedSelectors = null;
+ this._computedStyle = CssLogic.getComputedStyle(this.viewedElement);
+ }
+
+ /**
+ * Get the values of all the computed CSS properties for the highlighted
+ * element.
+ * @returns {object} The computed CSS properties for a selected element
+ */
+ get computedStyle() {
+ return this._computedStyle;
+ }
+
+ /**
+ * Get the source filter.
+ * @returns {string} The source filter being used.
+ */
+ get sourceFilter() {
+ return this._sourceFilter;
+ }
+
+ /**
+ * Source filter. Only display properties coming from the given source (web
+ * address). Note that in order to avoid information overload we DO NOT show
+ * unmatched system rules.
+ * @see FILTER.*
+ */
+ set sourceFilter(value) {
+ const oldValue = this._sourceFilter;
+ this._sourceFilter = value;
+
+ let ruleCount = 0;
+
+ // Update the CssSheet objects.
+ this.forEachSheet(function (sheet) {
+ if (sheet.authorSheet && sheet.sheetAllowed) {
+ ruleCount += sheet.ruleCount;
+ }
+ }, this);
+
+ this._ruleCount = ruleCount;
+
+ // Full update is needed because the this.processMatchedSelectors() method
+ // skips UA stylesheets if the filter does not allow such sheets.
+ const needFullUpdate = oldValue == FILTER.UA || value == FILTER.UA;
+
+ if (needFullUpdate) {
+ this._matchedRules = null;
+ this._matchedSelectors = null;
+ this._propertyInfos = {};
+ } else {
+ // Update the CssPropertyInfo objects.
+ for (const property in this._propertyInfos) {
+ this._propertyInfos[property].needRefilter = true;
+ }
+ }
+ }
+
+ /**
+ * Return a CssPropertyInfo data structure for the currently viewed element
+ * and the specified CSS property. If there is no currently viewed element we
+ * return an empty object.
+ *
+ * @param {string} property The CSS property to look for.
+ * @return {CssPropertyInfo} a CssPropertyInfo structure for the given
+ * property.
+ */
+ getPropertyInfo(property) {
+ if (!this.viewedElement) {
+ return {};
+ }
+
+ let info = this._propertyInfos[property];
+ if (!info) {
+ info = new CssPropertyInfo(this, property);
+ this._propertyInfos[property] = info;
+ }
+
+ return info;
+ }
+
+ /**
+ * Cache all the stylesheets in the inspected document
+ * @private
+ */
+ _cacheSheets() {
+ this._passId++;
+ this.reset();
+
+ // styleSheets isn't an array, but forEach can work on it anyway
+ const styleSheets = InspectorUtils.getAllStyleSheets(
+ this.viewedDocument,
+ true
+ );
+ Array.prototype.forEach.call(styleSheets, this._cacheSheet, this);
+
+ this._sheetsCached = true;
+ }
+
+ /**
+ * Cache a stylesheet if it falls within the requirements: if it's enabled,
+ * and if the @media is allowed. This method also walks through the stylesheet
+ * cssRules to find @imported rules, to cache the stylesheets of those rules
+ * as well. In addition, the @keyframes rules in the stylesheet are cached.
+ *
+ * @private
+ * @param {CSSStyleSheet} domSheet the CSSStyleSheet object to cache.
+ */
+ _cacheSheet(domSheet) {
+ if (domSheet.disabled) {
+ return;
+ }
+
+ // Only work with stylesheets that have their media allowed.
+ if (!this.mediaMatches(domSheet)) {
+ return;
+ }
+
+ // Cache the sheet.
+ const cssSheet = this.getSheet(domSheet, this._sheetIndex++);
+ if (cssSheet._passId != this._passId) {
+ cssSheet._passId = this._passId;
+
+ // Find import and keyframes rules.
+ for (const aDomRule of cssSheet.getCssRules()) {
+ const ruleClassName = ChromeUtils.getClassName(aDomRule);
+ if (
+ ruleClassName === "CSSImportRule" &&
+ aDomRule.styleSheet &&
+ this.mediaMatches(aDomRule)
+ ) {
+ this._cacheSheet(aDomRule.styleSheet);
+ } else if (ruleClassName === "CSSKeyframesRule") {
+ this._keyframesRules.push(aDomRule);
+ }
+ }
+ }
+ }
+
+ /**
+ * Retrieve the list of stylesheets in the document.
+ *
+ * @return {array} the list of stylesheets in the document.
+ */
+ get sheets() {
+ if (!this._sheetsCached) {
+ this._cacheSheets();
+ }
+
+ const sheets = [];
+ this.forEachSheet(function (sheet) {
+ if (sheet.authorSheet) {
+ sheets.push(sheet);
+ }
+ }, this);
+
+ return sheets;
+ }
+
+ /**
+ * Retrieve the list of keyframes rules in the document.
+ *
+ * @ return {array} the list of keyframes rules in the document.
+ */
+ get keyframesRules() {
+ if (!this._sheetsCached) {
+ this._cacheSheets();
+ }
+ return this._keyframesRules;
+ }
+
+ /**
+ * Retrieve a CssSheet object for a given a CSSStyleSheet object. If the
+ * stylesheet is already cached, you get the existing CssSheet object,
+ * otherwise the new CSSStyleSheet object is cached.
+ *
+ * @param {CSSStyleSheet} domSheet the CSSStyleSheet object you want.
+ * @param {number} index the index, within the document, of the stylesheet.
+ *
+ * @return {CssSheet} the CssSheet object for the given CSSStyleSheet object.
+ */
+ getSheet(domSheet, index) {
+ let cacheId = "";
+
+ if (domSheet.href) {
+ cacheId = domSheet.href;
+ } else if (domSheet.associatedDocument) {
+ cacheId = domSheet.associatedDocument.location;
+ }
+
+ let sheet = null;
+ let sheetFound = false;
+
+ if (cacheId in this._sheets) {
+ for (sheet of this._sheets[cacheId]) {
+ if (sheet.domSheet === domSheet) {
+ if (index != -1) {
+ sheet.index = index;
+ }
+ sheetFound = true;
+ break;
+ }
+ }
+ }
+
+ if (!sheetFound) {
+ if (!(cacheId in this._sheets)) {
+ this._sheets[cacheId] = [];
+ }
+
+ sheet = new CssSheet(this, domSheet, index);
+ if (sheet.sheetAllowed && sheet.authorSheet) {
+ this._ruleCount += sheet.ruleCount;
+ }
+
+ this._sheets[cacheId].push(sheet);
+ }
+
+ return sheet;
+ }
+
+ /**
+ * Process each cached stylesheet in the document using your callback.
+ *
+ * @param {function} callback the function you want executed for each of the
+ * CssSheet objects cached.
+ * @param {object} scope the scope you want for the callback function. scope
+ * will be the this object when callback executes.
+ */
+ forEachSheet(callback, scope) {
+ for (const cacheId in this._sheets) {
+ const sheets = this._sheets[cacheId];
+ for (let i = 0; i < sheets.length; i++) {
+ // We take this as an opportunity to clean dead sheets
+ try {
+ const sheet = sheets[i];
+ // If accessing domSheet raises an exception, then the style
+ // sheet is a dead object.
+ sheet.domSheet;
+ callback.call(scope, sheet, i, sheets);
+ } catch (e) {
+ sheets.splice(i, 1);
+ i--;
+ }
+ }
+ }
+ }
+
+ /**
+
+ /**
+ * Get the number CSSRule objects in the document, counted from all of
+ * the stylesheets. System sheets are excluded. If a filter is active, this
+ * tells only the number of CSSRule objects inside the selected
+ * CSSStyleSheet.
+ *
+ * WARNING: This only provides an estimate of the rule count, and the results
+ * could change at a later date. Todo remove this
+ *
+ * @return {number} the number of CSSRule (all rules).
+ */
+ get ruleCount() {
+ if (!this._sheetsCached) {
+ this._cacheSheets();
+ }
+
+ return this._ruleCount;
+ }
+
+ /**
+ * Process the CssSelector objects that match the highlighted element and its
+ * parent elements. scope.callback() is executed for each CssSelector
+ * object, being passed the CssSelector object and the match status.
+ *
+ * This method also includes all of the element.style properties, for each
+ * highlighted element parent and for the highlighted element itself.
+ *
+ * Note that the matched selectors are cached, such that next time your
+ * callback is invoked for the cached list of CssSelector objects.
+ *
+ * @param {function} callback the function you want to execute for each of
+ * the matched selectors.
+ * @param {object} scope the scope you want for the callback function. scope
+ * will be the this object when callback executes.
+ */
+ processMatchedSelectors(callback, scope) {
+ if (this._matchedSelectors) {
+ if (callback) {
+ this._passId++;
+ this._matchedSelectors.forEach(function (value) {
+ callback.call(scope, value[0], value[1]);
+ value[0].cssRule._passId = this._passId;
+ }, this);
+ }
+ return;
+ }
+
+ if (!this._matchedRules) {
+ this._buildMatchedRules();
+ }
+
+ this._matchedSelectors = [];
+ this._passId++;
+
+ for (const matchedRule of this._matchedRules) {
+ const [rule, status, distance] = matchedRule;
+
+ rule.selectors.forEach(function (selector) {
+ if (
+ selector._matchId !== this._matchId &&
+ (selector.inlineStyle ||
+ this.selectorMatchesElement(rule.domRule, selector.selectorIndex))
+ ) {
+ selector._matchId = this._matchId;
+ this._matchedSelectors.push([selector, status, distance]);
+ if (callback) {
+ callback.call(scope, selector, status, distance);
+ }
+ }
+ }, this);
+
+ rule._passId = this._passId;
+ }
+ }
+
+ /**
+ * Check if the given selector matches the highlighted element or any of its
+ * parents.
+ *
+ * @private
+ * @param {DOMRule} domRule
+ * The DOM Rule containing the selector.
+ * @param {Number} idx
+ * The index of the selector within the DOMRule.
+ * @return {boolean}
+ * true if the given selector matches the highlighted element or any
+ * of its parents, otherwise false is returned.
+ */
+ selectorMatchesElement(domRule, idx) {
+ let element = this.viewedElement;
+ do {
+ if (domRule.selectorMatchesElement(idx, element)) {
+ return true;
+ }
+ } while (
+ // Loop on flattenedTreeParentNode instead of parentNode to reach the
+ // shadow host from the shadow dom.
+ (element = element.flattenedTreeParentNode) &&
+ element.nodeType === nodeConstants.ELEMENT_NODE
+ );
+
+ return false;
+ }
+
+ /**
+ * Check if the highlighted element or it's parents have matched selectors.
+ *
+ * @param {Array} properties: The list of properties you want to check if they
+ * have matched selectors or not.
+ * @return {object} An object that tells for each property if it has matched
+ * selectors or not. Object keys are property names and values are booleans.
+ */
+ hasMatchedSelectors(properties) {
+ if (!this._matchedRules) {
+ this._buildMatchedRules();
+ }
+
+ const result = {};
+
+ this._matchedRules.some(function (value) {
+ const rule = value[0];
+ const status = value[1];
+ properties = properties.filter(property => {
+ // We just need to find if a rule has this property while it matches
+ // the viewedElement (or its parents).
+ if (
+ rule.getPropertyValue(property) &&
+ (status == STATUS.MATCHED ||
+ (status == STATUS.PARENT_MATCH &&
+ InspectorUtils.isInheritedProperty(
+ this.viewedDocument,
+ property
+ )))
+ ) {
+ result[property] = true;
+ return false;
+ }
+ // Keep the property for the next rule.
+ return true;
+ });
+ return !properties.length;
+ }, this);
+
+ return result;
+ }
+
+ /**
+ * Build the array of matched rules for the currently highlighted element.
+ * The array will hold rules that match the viewedElement and its parents.
+ *
+ * @private
+ */
+ _buildMatchedRules() {
+ let domRules;
+ let element = this.viewedElement;
+ const filter = this.sourceFilter;
+ let sheetIndex = 0;
+
+ // distance is used to tell us how close an ancestor is to an element e.g.
+ // 0: The rule is directly applied to the current element.
+ // -1: The rule is inherited from the current element's first parent.
+ // -2: The rule is inherited from the current element's second parent.
+ // etc.
+ let distance = 0;
+
+ this._matchId++;
+ this._passId++;
+ this._matchedRules = [];
+
+ if (!element) {
+ return;
+ }
+
+ do {
+ const status =
+ this.viewedElement === element ? STATUS.MATCHED : STATUS.PARENT_MATCH;
+
+ try {
+ domRules = getCSSStyleRules(element);
+ } catch (ex) {
+ console.log("CL__buildMatchedRules error: " + ex);
+ continue;
+ }
+
+ // Add element.style information. Order matters here, and style attribute wins over
+ // other rules, so we need to add it in `this._matchesRules` before the regular rules.
+ if (element.style && element.style.length) {
+ const rule = new CssRule(null, { style: element.style }, element);
+ rule._matchId = this._matchId;
+ rule._passId = this._passId;
+ this._matchedRules.push([rule, status, distance]);
+ }
+
+ // getCSSStyleRules can return null with a shadow DOM element.
+ if (domRules !== null) {
+ // getCSSStyleRules returns ordered from least-specific to most-specific,
+ // but we do want them from most-specific to least specific, so we need to loop
+ // through the rules backward.
+ for (let i = domRules.length - 1; i >= 0; i--) {
+ const domRule = domRules[i];
+ if (!CSSStyleRule.isInstance(domRule)) {
+ continue;
+ }
+
+ const sheet = this.getSheet(domRule.parentStyleSheet, -1);
+ if (sheet._passId !== this._passId) {
+ sheet.index = sheetIndex++;
+ sheet._passId = this._passId;
+ }
+
+ if (filter === FILTER.USER && !sheet.authorSheet) {
+ continue;
+ }
+
+ const rule = sheet.getRule(domRule);
+ if (rule._passId === this._passId) {
+ continue;
+ }
+
+ rule._matchId = this._matchId;
+ rule._passId = this._passId;
+ this._matchedRules.push([rule, status, distance]);
+ }
+ }
+
+ distance--;
+ } while (
+ // Loop on flattenedTreeParentNode instead of parentNode to reach the
+ // shadow host from the shadow dom.
+ (element = element.flattenedTreeParentNode) &&
+ element.nodeType === nodeConstants.ELEMENT_NODE
+ );
+ }
+
+ /**
+ * Tells if the given DOM CSS object matches the current view media.
+ *
+ * @param {object} domObject The DOM CSS object to check.
+ * @return {boolean} True if the DOM CSS object matches the current view
+ * media, or false otherwise.
+ */
+ mediaMatches(domObject) {
+ const mediaText = domObject.media.mediaText;
+ return (
+ !mediaText ||
+ this.viewedDocument.defaultView.matchMedia(mediaText).matches
+ );
+ }
+}
+
+/**
+ * If the element has an id, return '#id'. Otherwise return 'tagname[n]' where
+ * n is the index of this element in its siblings.
+ * <p>A technically more 'correct' output from the no-id case might be:
+ * 'tagname:nth-of-type(n)' however this is unlikely to be more understood
+ * and it is longer.
+ *
+ * @param {Element} element the element for which you want the short name.
+ * @return {string} the string to be displayed for element.
+ */
+CssLogic.getShortName = function (element) {
+ if (!element) {
+ return "null";
+ }
+ if (element.id) {
+ return "#" + element.id;
+ }
+ let priorSiblings = 0;
+ let temp = element;
+ while ((temp = temp.previousElementSibling)) {
+ priorSiblings++;
+ }
+ return element.tagName + "[" + priorSiblings + "]";
+};
+
+/**
+ * Get a string list of selectors for a given DOMRule.
+ *
+ * @param {DOMRule} domRule
+ * The DOMRule to parse.
+ * @param {Boolean} desugared
+ * Set to true to get the desugared selector (see https://drafts.csswg.org/css-nesting-1/#nest-selector)
+ * @return {Array}
+ * An array of string selectors.
+ */
+CssLogic.getSelectors = function (domRule, desugared = false) {
+ if (ChromeUtils.getClassName(domRule) !== "CSSStyleRule") {
+ // Return empty array since CSSRule#selectorCount assumes only STYLE_RULE type.
+ return [];
+ }
+
+ const selectors = [];
+
+ const len = domRule.selectorCount;
+ for (let i = 0; i < len; i++) {
+ selectors.push(domRule.selectorTextAt(i, desugared));
+ }
+ return selectors;
+};
+
+/**
+ * Given a node, check to see if it is a ::before or ::after element.
+ * If so, return the node that is accessible from within the document
+ * (the parent of the anonymous node), along with which pseudo element
+ * it was. Otherwise, return the node itself.
+ *
+ * @returns {Object}
+ * - {DOMNode} node The non-anonymous node
+ * - {string} pseudo One of ':marker', ':before', ':after', or null.
+ */
+CssLogic.getBindingElementAndPseudo = getBindingElementAndPseudo;
+
+/**
+ * Get the computed style on a node. Automatically handles reading
+ * computed styles on a ::before/::after element by reading on the
+ * parent node with the proper pseudo argument.
+ *
+ * @param {Node}
+ * @returns {CSSStyleDeclaration}
+ */
+CssLogic.getComputedStyle = function (node) {
+ if (
+ !node ||
+ Cu.isDeadWrapper(node) ||
+ node.nodeType !== nodeConstants.ELEMENT_NODE ||
+ !node.ownerGlobal
+ ) {
+ return null;
+ }
+
+ const { bindingElement, pseudo } = CssLogic.getBindingElementAndPseudo(node);
+
+ // For reasons that still escape us, pseudo-elements can sometimes be "unattached" (i.e.
+ // not have a parentNode defined). This seems to happen when a page is reloaded while
+ // the inspector is open. Bailing out here ensures that the inspector does not fail at
+ // presenting DOM nodes and CSS styles when this happens. This is a temporary measure.
+ // See bug 1506792.
+ if (!bindingElement) {
+ return null;
+ }
+
+ return node.ownerGlobal.getComputedStyle(bindingElement, pseudo);
+};
+
+/**
+ * Get a source for a stylesheet, taking into account embedded stylesheets
+ * for which we need to use document.defaultView.location.href rather than
+ * sheet.href
+ *
+ * @param {CSSStyleSheet} sheet the DOM object for the style sheet.
+ * @return {string} the address of the stylesheet.
+ */
+CssLogic.href = function (sheet) {
+ return sheet.href || sheet.associatedDocument.location;
+};
+
+/**
+ * Returns true if the given node has visited state.
+ */
+CssLogic.hasVisitedState = hasVisitedState;
+
+class CssSheet {
+ /**
+ * A safe way to access cached bits of information about a stylesheet.
+ *
+ * @constructor
+ * @param {CssLogic} cssLogic pointer to the CssLogic instance working with
+ * this CssSheet object.
+ * @param {CSSStyleSheet} domSheet reference to a DOM CSSStyleSheet object.
+ * @param {number} index tells the index/position of the stylesheet within the
+ * main document.
+ */
+ constructor(cssLogic, domSheet, index) {
+ this._cssLogic = cssLogic;
+ this.domSheet = domSheet;
+ this.index = this.authorSheet ? index : -100 * index;
+
+ // Cache of the sheets href. Cached by the getter.
+ this._href = null;
+ // Short version of href for use in select boxes etc. Cached by getter.
+ this._shortSource = null;
+
+ // null for uncached.
+ this._sheetAllowed = null;
+
+ // Cached CssRules from the given stylesheet.
+ this._rules = {};
+
+ this._ruleCount = -1;
+ }
+
+ _passId = null;
+ _agentSheet = null;
+ _authorSheet = null;
+ _userSheet = null;
+
+ /**
+ * Check if the stylesheet is an agent stylesheet (provided by the browser).
+ *
+ * @return {boolean} true if this is an agent stylesheet, false otherwise.
+ */
+ get agentSheet() {
+ if (this._agentSheet === null) {
+ this._agentSheet = isAgentStylesheet(this.domSheet);
+ }
+ return this._agentSheet;
+ }
+
+ /**
+ * Check if the stylesheet is an author stylesheet (provided by the content page).
+ *
+ * @return {boolean} true if this is an author stylesheet, false otherwise.
+ */
+ get authorSheet() {
+ if (this._authorSheet === null) {
+ this._authorSheet = isAuthorStylesheet(this.domSheet);
+ }
+ return this._authorSheet;
+ }
+
+ /**
+ * Check if the stylesheet is a user stylesheet (provided by userChrome.css or
+ * userContent.css).
+ *
+ * @return {boolean} true if this is a user stylesheet, false otherwise.
+ */
+ get userSheet() {
+ if (this._userSheet === null) {
+ this._userSheet = isUserStylesheet(this.domSheet);
+ }
+ return this._userSheet;
+ }
+
+ /**
+ * Check if the stylesheet is disabled or not.
+ * @return {boolean} true if this stylesheet is disabled, or false otherwise.
+ */
+ get disabled() {
+ return this.domSheet.disabled;
+ }
+
+ /**
+ * Get a source for a stylesheet, using CssLogic.href
+ *
+ * @return {string} the address of the stylesheet.
+ */
+ get href() {
+ if (this._href) {
+ return this._href;
+ }
+
+ this._href = CssLogic.href(this.domSheet);
+ return this._href;
+ }
+
+ /**
+ * Create a shorthand version of the href of a stylesheet.
+ *
+ * @return {string} the shorthand source of the stylesheet.
+ */
+ get shortSource() {
+ if (this._shortSource) {
+ return this._shortSource;
+ }
+
+ this._shortSource = shortSource(this.domSheet);
+ return this._shortSource;
+ }
+
+ /**
+ * Tells if the sheet is allowed or not by the current CssLogic.sourceFilter.
+ *
+ * @return {boolean} true if the stylesheet is allowed by the sourceFilter, or
+ * false otherwise.
+ */
+ get sheetAllowed() {
+ if (this._sheetAllowed !== null) {
+ return this._sheetAllowed;
+ }
+
+ this._sheetAllowed = true;
+
+ const filter = this._cssLogic.sourceFilter;
+ if (filter === FILTER.USER && !this.authorSheet) {
+ this._sheetAllowed = false;
+ }
+ if (filter !== FILTER.USER && filter !== FILTER.UA) {
+ this._sheetAllowed = filter === this.href;
+ }
+
+ return this._sheetAllowed;
+ }
+
+ /**
+ * Retrieve the number of rules in this stylesheet.
+ *
+ * @return {number} the number of CSSRule objects in this stylesheet.
+ */
+ get ruleCount() {
+ try {
+ return this._ruleCount > -1 ? this._ruleCount : this.getCssRules().length;
+ } catch (e) {
+ return 0;
+ }
+ }
+
+ /**
+ * Retrieve the array of css rules for this stylesheet.
+ *
+ * Accessing cssRules on a stylesheet that is not completely loaded can throw a
+ * DOMException (Bug 625013). This wrapper will return an empty array instead.
+ *
+ * @return {Array} array of css rules.
+ **/
+ getCssRules() {
+ try {
+ return this.domSheet.cssRules;
+ } catch (e) {
+ return [];
+ }
+ }
+
+ /**
+ * Retrieve a CssRule object for the given CSSStyleRule. The CssRule object is
+ * cached, such that subsequent retrievals return the same CssRule object for
+ * the same CSSStyleRule object.
+ *
+ * @param {CSSStyleRule} aDomRule the CSSStyleRule object for which you want a
+ * CssRule object.
+ * @return {CssRule} the cached CssRule object for the given CSSStyleRule
+ * object.
+ */
+ getRule(domRule) {
+ const cacheId = domRule.type + domRule.selectorText;
+
+ let rule = null;
+ let ruleFound = false;
+
+ if (cacheId in this._rules) {
+ for (rule of this._rules[cacheId]) {
+ if (rule.domRule === domRule) {
+ ruleFound = true;
+ break;
+ }
+ }
+ }
+
+ if (!ruleFound) {
+ if (!(cacheId in this._rules)) {
+ this._rules[cacheId] = [];
+ }
+
+ rule = new CssRule(this, domRule);
+ this._rules[cacheId].push(rule);
+ }
+
+ return rule;
+ }
+
+ toString() {
+ return "CssSheet[" + this.shortSource + "]";
+ }
+}
+
+class CssRule {
+ /**
+ * Information about a single CSSStyleRule.
+ *
+ * @param {CSSStyleSheet|null} cssSheet the CssSheet object of the stylesheet that
+ * holds the CSSStyleRule. If the rule comes from element.style, set this
+ * argument to null.
+ * @param {CSSStyleRule|object} domRule the DOM CSSStyleRule for which you want
+ * to cache data. If the rule comes from element.style, then provide
+ * an object of the form: {style: element.style}.
+ * @param {Element} [element] If the rule comes from element.style, then this
+ * argument must point to the element.
+ * @constructor
+ */
+ constructor(cssSheet, domRule, element) {
+ this._cssSheet = cssSheet;
+ this.domRule = domRule;
+
+ if (this._cssSheet) {
+ // parse domRule.selectorText on call to this.selectors
+ this._selectors = null;
+ this.line = InspectorUtils.getRelativeRuleLine(this.domRule);
+ this.column = InspectorUtils.getRuleColumn(this.domRule);
+ this.href = this._cssSheet.href;
+ this.authorRule = this._cssSheet.authorSheet;
+ this.userRule = this._cssSheet.userSheet;
+ this.agentRule = this._cssSheet.agentSheet;
+ } else if (element) {
+ this._selectors = [new CssSelector(this, "@element.style", 0)];
+ this.line = -1;
+ this.href = "#";
+ this.authorRule = true;
+ this.userRule = false;
+ this.agentRule = false;
+ this.sourceElement = element;
+ }
+ }
+
+ _passId = null;
+
+ /**
+ * Check if the parent stylesheet is allowed by the CssLogic.sourceFilter.
+ *
+ * @return {boolean} true if the parent stylesheet is allowed by the current
+ * sourceFilter, or false otherwise.
+ */
+ get sheetAllowed() {
+ return this._cssSheet ? this._cssSheet.sheetAllowed : true;
+ }
+
+ /**
+ * Retrieve the parent stylesheet index/position in the viewed document.
+ *
+ * @return {number} the parent stylesheet index/position in the viewed
+ * document.
+ */
+ get sheetIndex() {
+ return this._cssSheet ? this._cssSheet.index : 0;
+ }
+
+ /**
+ * Retrieve the style property value from the current CSSStyleRule.
+ *
+ * @param {string} property the CSS property name for which you want the
+ * value.
+ * @return {string} the property value.
+ */
+ getPropertyValue(property) {
+ return this.domRule.style.getPropertyValue(property);
+ }
+
+ /**
+ * Retrieve the style property priority from the current CSSStyleRule.
+ *
+ * @param {string} property the CSS property name for which you want the
+ * priority.
+ * @return {string} the property priority.
+ */
+ getPropertyPriority(property) {
+ return this.domRule.style.getPropertyPriority(property);
+ }
+
+ /**
+ * Retrieve the list of CssSelector objects for each of the parsed selectors
+ * of the current CSSStyleRule.
+ *
+ * @return {array} the array hold the CssSelector objects.
+ */
+ get selectors() {
+ if (this._selectors) {
+ return this._selectors;
+ }
+
+ // Parse the CSSStyleRule.selectorText string.
+ this._selectors = [];
+
+ if (!this.domRule.selectorText) {
+ return this._selectors;
+ }
+
+ const selectors = CssLogic.getSelectors(this.domRule);
+
+ for (let i = 0, len = selectors.length; i < len; i++) {
+ this._selectors.push(new CssSelector(this, selectors[i], i));
+ }
+
+ return this._selectors;
+ }
+
+ toString() {
+ return "[CssRule " + this.domRule.selectorText + "]";
+ }
+}
+
+class CssSelector {
+ /**
+ * The CSS selector class allows us to document the ranking of various CSS
+ * selectors.
+ *
+ * @constructor
+ * @param {CssRule} cssRule the CssRule instance from where the selector comes.
+ * @param {string} selector The selector that we wish to investigate.
+ * @param {Number} index The index of the selector within it's rule.
+ */
+ constructor(cssRule, selector, index) {
+ this.cssRule = cssRule;
+ this.text = selector;
+ this.inlineStyle = this.text == "@element.style";
+ this._specificity = null;
+ this.selectorIndex = index;
+ }
+
+ _matchId = null;
+
+ /**
+ * Retrieve the CssSelector source element, which is the source of the CssRule
+ * owning the selector. This is only available when the CssSelector comes from
+ * an element.style.
+ *
+ * @return {string} the source element selector.
+ */
+ get sourceElement() {
+ return this.cssRule.sourceElement;
+ }
+
+ /**
+ * Retrieve the address of the CssSelector. This points to the address of the
+ * CssSheet owning this selector.
+ *
+ * @return {string} the address of the CssSelector.
+ */
+ get href() {
+ return this.cssRule.href;
+ }
+
+ /**
+ * Check if the selector comes from an agent stylesheet (provided by the browser).
+ *
+ * @return {boolean} true if this is an agent stylesheet, false otherwise.
+ */
+ get agentRule() {
+ return this.cssRule.agentRule;
+ }
+
+ /**
+ * Check if the selector comes from an author stylesheet (provided by the content page).
+ *
+ * @return {boolean} true if this is an author stylesheet, false otherwise.
+ */
+ get authorRule() {
+ return this.cssRule.authorRule;
+ }
+
+ /**
+ * Check if the selector comes from a user stylesheet (provided by userChrome.css or
+ * userContent.css).
+ *
+ * @return {boolean} true if this is a user stylesheet, false otherwise.
+ */
+ get userRule() {
+ return this.cssRule.userRule;
+ }
+
+ /**
+ * Check if the parent stylesheet is allowed by the CssLogic.sourceFilter.
+ *
+ * @return {boolean} true if the parent stylesheet is allowed by the current
+ * sourceFilter, or false otherwise.
+ */
+ get sheetAllowed() {
+ return this.cssRule.sheetAllowed;
+ }
+
+ /**
+ * Retrieve the parent stylesheet index/position in the viewed document.
+ *
+ * @return {number} the parent stylesheet index/position in the viewed
+ * document.
+ */
+ get sheetIndex() {
+ return this.cssRule.sheetIndex;
+ }
+
+ /**
+ * Retrieve the line of the parent CSSStyleRule in the parent CSSStyleSheet.
+ *
+ * @return {number} the line of the parent CSSStyleRule in the parent
+ * stylesheet.
+ */
+ get ruleLine() {
+ return this.cssRule.line;
+ }
+
+ /**
+ * Retrieve the column of the parent CSSStyleRule in the parent CSSStyleSheet.
+ *
+ * @return {number} the column of the parent CSSStyleRule in the parent
+ * stylesheet.
+ */
+ get ruleColumn() {
+ return this.cssRule.column;
+ }
+
+ /**
+ * Retrieve specificity information for the current selector.
+ *
+ * @see http://www.w3.org/TR/css3-selectors/#specificity
+ * @see http://www.w3.org/TR/CSS2/selector.html
+ *
+ * @return {Number} The selector's specificity.
+ */
+ get specificity() {
+ if (this.inlineStyle) {
+ // We can't ask specificity from DOMUtils as element styles don't provide
+ // CSSStyleRule interface DOMUtils expect. However, specificity of element
+ // style is constant, 1,0,0,0 or 0x40000000, just return the constant
+ // directly. @see http://www.w3.org/TR/CSS2/cascade.html#specificity
+ return 0x40000000;
+ }
+
+ if (typeof this._specificity !== "number") {
+ this._specificity = this.cssRule.domRule.selectorSpecificityAt(
+ this.selectorIndex
+ );
+ }
+
+ return this._specificity;
+ }
+
+ toString() {
+ return this.text;
+ }
+}
+
+class CssPropertyInfo {
+ /**
+ * A cache of information about the matched rules, selectors and values attached
+ * to a CSS property, for the highlighted element.
+ *
+ * The heart of the CssPropertyInfo object is the _findMatchedSelectors()
+ * method. This are invoked when the PropertyView tries to access the
+ * .matchedSelectors array.
+ * Results are cached, for later reuse.
+ *
+ * @param {CssLogic} cssLogic Reference to the parent CssLogic instance
+ * @param {string} property The CSS property we are gathering information for
+ * @constructor
+ */
+ constructor(cssLogic, property) {
+ this._cssLogic = cssLogic;
+ this.property = property;
+ this._value = "";
+
+ // An array holding CssSelectorInfo objects for each of the matched selectors
+ // that are inside a CSS rule. Only rules that hold the this.property are
+ // counted. This includes rules that come from filtered stylesheets (those
+ // that have sheetAllowed = false).
+ this._matchedSelectors = null;
+ }
+
+ /**
+ * Retrieve the computed style value for the current property, for the
+ * highlighted element.
+ *
+ * @return {string} the computed style value for the current property, for the
+ * highlighted element.
+ */
+ get value() {
+ if (!this._value && this._cssLogic.computedStyle) {
+ try {
+ this._value = this._cssLogic.computedStyle.getPropertyValue(
+ this.property
+ );
+ } catch (ex) {
+ console.log("Error reading computed style for " + this.property);
+ console.log(ex);
+ }
+ }
+ return this._value;
+ }
+
+ /**
+ * Retrieve the array holding CssSelectorInfo objects for each of the matched
+ * selectors, from each of the matched rules. Only selectors coming from
+ * allowed stylesheets are included in the array.
+ *
+ * @return {array} the list of CssSelectorInfo objects of selectors that match
+ * the highlighted element and its parents.
+ */
+ get matchedSelectors() {
+ if (!this._matchedSelectors) {
+ this._findMatchedSelectors();
+ } else if (this.needRefilter) {
+ this._refilterSelectors();
+ }
+
+ return this._matchedSelectors;
+ }
+
+ /**
+ * Find the selectors that match the highlighted element and its parents.
+ * Uses CssLogic.processMatchedSelectors() to find the matched selectors,
+ * passing in a reference to CssPropertyInfo._processMatchedSelector() to
+ * create CssSelectorInfo objects, which we then sort
+ * @private
+ */
+ _findMatchedSelectors() {
+ this._matchedSelectors = [];
+ this.needRefilter = false;
+
+ this._cssLogic.processMatchedSelectors(this._processMatchedSelector, this);
+
+ // Sort the selectors by how well they match the given element.
+ this._matchedSelectors.sort((selectorInfo1, selectorInfo2) =>
+ selectorInfo1.compareTo(selectorInfo2, this._matchedSelectors)
+ );
+
+ // Now we know which of the matches is best, we can mark it BEST_MATCH.
+ if (
+ this._matchedSelectors.length &&
+ this._matchedSelectors[0].status > STATUS.UNMATCHED
+ ) {
+ this._matchedSelectors[0].status = STATUS.BEST;
+ }
+ }
+
+ /**
+ * Process a matched CssSelector object.
+ *
+ * @private
+ * @param {CssSelector} selector: the matched CssSelector object.
+ * @param {STATUS} status: the CssSelector match status.
+ * @param {Int} distance: See CssLogic._buildMatchedRules for definition.
+ */
+ _processMatchedSelector(selector, status, distance) {
+ const cssRule = selector.cssRule;
+ const value = cssRule.getPropertyValue(this.property);
+ if (
+ value &&
+ (status == STATUS.MATCHED ||
+ (status == STATUS.PARENT_MATCH &&
+ InspectorUtils.isInheritedProperty(
+ this._cssLogic.viewedDocument,
+ this.property
+ )))
+ ) {
+ const selectorInfo = new CssSelectorInfo(
+ selector,
+ this.property,
+ value,
+ status,
+ distance
+ );
+ this._matchedSelectors.push(selectorInfo);
+ }
+ }
+
+ /**
+ * Refilter the matched selectors array when the CssLogic.sourceFilter
+ * changes. This allows for quick filter changes.
+ * @private
+ */
+ _refilterSelectors() {
+ const passId = ++this._cssLogic._passId;
+
+ const iterator = function (selectorInfo) {
+ const cssRule = selectorInfo.selector.cssRule;
+ if (cssRule._passId != passId) {
+ cssRule._passId = passId;
+ }
+ };
+
+ if (this._matchedSelectors) {
+ this._matchedSelectors.forEach(iterator);
+ }
+
+ this.needRefilter = false;
+ }
+
+ toString() {
+ return "CssPropertyInfo[" + this.property + "]";
+ }
+}
+
+class CssSelectorInfo {
+ /**
+ * A class that holds information about a given CssSelector object.
+ *
+ * Instances of this class are given to CssHtmlTree in the array of matched
+ * selectors. Each such object represents a displayable row in the PropertyView
+ * objects. The information given by this object blends data coming from the
+ * CssSheet, CssRule and from the CssSelector that own this object.
+ *
+ * @param {CssSelector} selector The CssSelector object for which to
+ * present information.
+ * @param {string} property The property for which information should
+ * be retrieved.
+ * @param {string} value The property value from the CssRule that owns
+ * the selector.
+ * @param {STATUS} status The selector match status.
+ * @param {number} distance See CssLogic._buildMatchedRules for definition.
+ * @constructor
+ */
+ constructor(selector, property, value, status, distance) {
+ this.selector = selector;
+ this.property = property;
+ this.status = status;
+ this.distance = distance;
+ this.value = value;
+ const priority = this.selector.cssRule.getPropertyPriority(this.property);
+ this.important = priority === "important";
+
+ // Array<string|CSSLayerBlockRule>
+ this.parentLayers = [];
+
+ // Go through all parent rules to populate this.parentLayers
+ let rule = selector.cssRule.domRule;
+ while (rule) {
+ const className = ChromeUtils.getClassName(rule);
+ if (className == "CSSLayerBlockRule") {
+ // If the layer has a name, it's enough to uniquely identify it
+ // If the layer does not have a name. We put the actual rule here, so we'll
+ // be able to compare actual rule instances in `compareTo`
+ this.parentLayers.push(rule.name || rule);
+ } else if (className == "CSSImportRule" && rule.layerName !== null) {
+ // Same reasoning for @import rule + layer
+ this.parentLayers.push(rule.layerName || rule);
+ }
+
+ // Get the parent rule (could be the parent stylesheet owner rule
+ // for `@import url(path/to/file.css) layer`)
+ rule = rule.parentRule || rule.parentStyleSheet?.ownerRule;
+ }
+ }
+
+ /**
+ * Retrieve the CssSelector source element, which is the source of the CssRule
+ * owning the selector. This is only available when the CssSelector comes from
+ * an element.style.
+ *
+ * @return {string} the source element selector.
+ */
+ get sourceElement() {
+ return this.selector.sourceElement;
+ }
+
+ /**
+ * Retrieve the address of the CssSelector. This points to the address of the
+ * CssSheet owning this selector.
+ *
+ * @return {string} the address of the CssSelector.
+ */
+ get href() {
+ return this.selector.href;
+ }
+
+ /**
+ * Check if the CssSelector comes from element.style or not.
+ *
+ * @return {boolean} true if the CssSelector comes from element.style, or
+ * false otherwise.
+ */
+ get inlineStyle() {
+ return this.selector.inlineStyle;
+ }
+
+ /**
+ * Retrieve specificity information for the current selector.
+ *
+ * @return {object} an object holding specificity information for the current
+ * selector.
+ */
+ get specificity() {
+ return this.selector.specificity;
+ }
+
+ /**
+ * Retrieve the parent stylesheet index/position in the viewed document.
+ *
+ * @return {number} the parent stylesheet index/position in the viewed
+ * document.
+ */
+ get sheetIndex() {
+ return this.selector.sheetIndex;
+ }
+
+ /**
+ * Check if the parent stylesheet is allowed by the CssLogic.sourceFilter.
+ *
+ * @return {boolean} true if the parent stylesheet is allowed by the current
+ * sourceFilter, or false otherwise.
+ */
+ get sheetAllowed() {
+ return this.selector.sheetAllowed;
+ }
+
+ /**
+ * Retrieve the line of the parent CSSStyleRule in the parent CSSStyleSheet.
+ *
+ * @return {number} the line of the parent CSSStyleRule in the parent
+ * stylesheet.
+ */
+ get ruleLine() {
+ return this.selector.ruleLine;
+ }
+
+ /**
+ * Retrieve the column of the parent CSSStyleRule in the parent CSSStyleSheet.
+ *
+ * @return {number} the column of the parent CSSStyleRule in the parent
+ * stylesheet.
+ */
+ get ruleColumn() {
+ return this.selector.ruleColumn;
+ }
+
+ /**
+ * Check if the selector comes from a browser-provided stylesheet.
+ *
+ * @return {boolean} true if the selector comes from a browser-provided
+ * stylesheet, or false otherwise.
+ */
+ get agentRule() {
+ return this.selector.agentRule;
+ }
+
+ /**
+ * Check if the selector comes from a webpage-provided stylesheet.
+ *
+ * @return {boolean} true if the selector comes from a webpage-provided
+ * stylesheet, or false otherwise.
+ */
+ get authorRule() {
+ return this.selector.authorRule;
+ }
+
+ /**
+ * Check if the selector comes from a user stylesheet (userChrome.css or
+ * userContent.css).
+ *
+ * @return {boolean} true if the selector comes from a webpage-provided
+ * stylesheet, or false otherwise.
+ */
+ get userRule() {
+ return this.selector.userRule;
+ }
+
+ /**
+ * Compare the current CssSelectorInfo instance to another instance.
+ * Since selectorInfos is computed from `InspectorUtils.getCSSStyleRules`,
+ * it's already sorted for regular cases. We only need to handle important values.
+ *
+ * @param {CssSelectorInfo} that
+ * The instance to compare ourselves against.
+ * @param {Array<CssSelectorInfo>} selectorInfos
+ * The list of CssSelectorInfo we are currently ordering
+ * @return {Number}
+ * -1, 0, 1 depending on how that compares with this.
+ */
+ compareTo(that, selectorInfos) {
+ const originalOrder =
+ selectorInfos.indexOf(this) < selectorInfos.indexOf(that) ? -1 : 1;
+
+ // If both properties are not important, we can keep the original order
+ if (!this.important && !that.important) {
+ return originalOrder;
+ }
+
+ // If one of the property is important and the other is not, the important one wins
+ if (this.important !== that.important) {
+ return this.important ? -1 : 1;
+ }
+
+ // At this point, this and that are both important
+
+ const thisIsInLayer = !!this.parentLayers.length;
+ const thatIsInLayer = !!that.parentLayers.length;
+
+ // If they're not in layers, we can keep the original rule order
+ if (!thisIsInLayer && !thatIsInLayer) {
+ return originalOrder;
+ }
+
+ // If one of the rule is the style attribute, it wins
+ if (this.selector.inlineStyle || that.selector.inlineStyle) {
+ return this.selector.inlineStyle ? -1 : 1;
+ }
+
+ // If one of the rule is not in a layer, then the rule in a layer wins.
+ if (!thisIsInLayer || !thatIsInLayer) {
+ return thisIsInLayer ? -1 : 1;
+ }
+
+ const inSameLayers =
+ this.parentLayers.length === that.parentLayers.length &&
+ this.parentLayers.every((layer, i) => layer === that.parentLayers[i]);
+ // If both rules are in the same layer, we keep the original order
+ if (inSameLayers) {
+ return originalOrder;
+ }
+
+ // When comparing declarations that belong to different layers, then for
+ // important rules the declaration whose cascade layer is first wins.
+ // We get the rules in the most-specific to least-specific order, meaning we'll have
+ // rules in layers in the reverse order of the order of declarations of layers.
+ // We can reverse that again to get the order of declarations of layers.
+ return originalOrder * -1;
+ }
+
+ compare(that, propertyName, type) {
+ switch (type) {
+ case COMPAREMODE.BOOLEAN:
+ if (this[propertyName] && !that[propertyName]) {
+ return -1;
+ }
+ if (!this[propertyName] && that[propertyName]) {
+ return 1;
+ }
+ break;
+ case COMPAREMODE.INTEGER:
+ if (this[propertyName] > that[propertyName]) {
+ return -1;
+ }
+ if (this[propertyName] < that[propertyName]) {
+ return 1;
+ }
+ break;
+ }
+ return 0;
+ }
+
+ toString() {
+ return this.selector + " -> " + this.value;
+ }
+}
+
+exports.CssLogic = CssLogic;
+exports.CssSelector = CssSelector;