/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* 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 nsIDOMCSSRule 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 { Cc, Ci, Cu } = require("chrome"); const DevToolsUtils = require("devtools/shared/DevToolsUtils"); const { getRootBindingParent } = require("devtools/shared/layout/utils"); const nodeConstants = require("devtools/shared/dom-node-constants"); const {l10n, isContentStylesheet, shortSource, FILTER, STATUS} = require("devtools/shared/inspector/css-logic"); loader.lazyRequireGetter(this, "CSSLexer", "devtools/shared/css/lexer"); /** * @param {function} isInherited A function that determines if the CSS property * is inherited. */ function CssLogic(isInherited) { // The cache of examined CSS properties. this._isInherited = isInherited; this._propertyInfos = {}; } exports.CssLogic = CssLogic; CssLogic.prototype = { // 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: function () { 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 {nsIDOMElement} aViewedElement the element the user has highlighted * in the Inspector. */ highlight: function (viewedElement) { if (!viewedElement) { this.viewedElement = null; this.viewedDocument = null; this._computedStyle = null; this.reset(); return; } if (viewedElement === this.viewedElement) { return; } this.viewedElement = viewedElement; let 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) { let oldValue = this._sourceFilter; this._sourceFilter = value; let ruleCount = 0; // Update the CssSheet objects. this.forEachSheet(function (sheet) { sheet._sheetAllowed = -1; if (sheet.contentSheet && 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. let needFullUpdate = (oldValue == FILTER.UA || value == FILTER.UA); if (needFullUpdate) { this._matchedRules = null; this._matchedSelectors = null; this._propertyInfos = {}; } else { // Update the CssPropertyInfo objects. for (let 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: function (property) { if (!this.viewedElement) { return {}; } let info = this._propertyInfos[property]; if (!info) { info = new CssPropertyInfo(this, property, this._isInherited); this._propertyInfos[property] = info; } return info; }, /** * Cache all the stylesheets in the inspected document * @private */ _cacheSheets: function () { this._passId++; this.reset(); // styleSheets isn't an array, but forEach can work on it anyway Array.prototype.forEach.call(this.viewedDocument.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: function (domSheet) { if (domSheet.disabled) { return; } // Only work with stylesheets that have their media allowed. if (!this.mediaMatches(domSheet)) { return; } // Cache the sheet. let cssSheet = this.getSheet(domSheet, this._sheetIndex++); if (cssSheet._passId != this._passId) { cssSheet._passId = this._passId; // Find import and keyframes rules. for (let aDomRule of domSheet.cssRules) { if (aDomRule.type == CSSRule.IMPORT_RULE && aDomRule.styleSheet && this.mediaMatches(aDomRule)) { this._cacheSheet(aDomRule.styleSheet); } else if (aDomRule.type == CSSRule.KEYFRAMES_RULE) { 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(); } let sheets = []; this.forEachSheet(function (sheet) { if (sheet.contentSheet) { 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: function (domSheet, index) { let cacheId = ""; if (domSheet.href) { cacheId = domSheet.href; } else if (domSheet.ownerNode && domSheet.ownerNode.ownerDocument) { cacheId = domSheet.ownerNode.ownerDocument.location; } let sheet = null; let sheetFound = false; if (cacheId in this._sheets) { for (let i = 0, numSheets = this._sheets[cacheId].length; i < numSheets; i++) { sheet = this._sheets[cacheId][i]; 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.contentSheet) { 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: function (callback, scope) { for (let cacheId in this._sheets) { let sheets = this._sheets[cacheId]; for (let i = 0; i < sheets.length; i++) { // We take this as an opportunity to clean dead sheets try { let 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 nsIDOMCSSRule 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 nsIDOMCSSRule 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 nsIDOMCSSRule (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: function (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 (let i = 0; i < this._matchedRules.length; i++) { let rule = this._matchedRules[i][0]; let status = this._matchedRules[i][1]; rule.selectors.forEach(function (selector) { if (selector._matchId !== this._matchId && (selector.elementStyle || this.selectorMatchesElement(rule.domRule, selector.selectorIndex))) { selector._matchId = this._matchId; this._matchedSelectors.push([ selector, status ]); if (callback) { callback.call(scope, selector, status); } } }, 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: function (domRule, idx) { let element = this.viewedElement; do { if (domUtils.selectorMatchesElement(element, domRule, idx)) { return true; } } while ((element = element.parentNode) && element.nodeType === nodeConstants.ELEMENT_NODE); return false; }, /** * Check if the highlighted element or it's parents have matched selectors. * * @param {array} aProperties 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: function (properties) { if (!this._matchedRules) { this._buildMatchedRules(); } let result = {}; this._matchedRules.some(function (value) { let rule = value[0]; let 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 && this._isInherited(property)))) { result[property] = true; return false; } // Keep the property for the next rule. return true; }); return properties.length == 0; }, 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: function () { let domRules; let element = this.viewedElement; let filter = this.sourceFilter; let sheetIndex = 0; this._matchId++; this._passId++; this._matchedRules = []; if (!element) { return; } do { let status = this.viewedElement === element ? STATUS.MATCHED : STATUS.PARENT_MATCH; try { // Handle finding rules on pseudo by reading style rules // on the parent node with proper pseudo arg to getCSSStyleRules. let {bindingElement, pseudo} = CssLogic.getBindingElementAndPseudo(element); domRules = domUtils.getCSSStyleRules(bindingElement, pseudo); } catch (ex) { console.log("CL__buildMatchedRules error: " + ex); continue; } // getCSSStyleRules can return null with a shadow DOM element. let numDomRules = domRules ? domRules.Count() : 0; for (let i = 0; i < numDomRules; i++) { let domRule = domRules.GetElementAt(i); if (domRule.type !== CSSRule.STYLE_RULE) { continue; } let sheet = this.getSheet(domRule.parentStyleSheet, -1); if (sheet._passId !== this._passId) { sheet.index = sheetIndex++; sheet._passId = this._passId; } if (filter === FILTER.USER && !sheet.contentSheet) { continue; } let rule = sheet.getRule(domRule); if (rule._passId === this._passId) { continue; } rule._matchId = this._matchId; rule._passId = this._passId; this._matchedRules.push([rule, status]); } // Add element.style information. if (element.style && element.style.length > 0) { let rule = new CssRule(null, { style: element.style }, element); rule._matchId = this._matchId; rule._passId = this._passId; this._matchedRules.push([rule, status]); } } while ((element = element.parentNode) && 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: function (domObject) { let 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. *

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 {nsIDOMElement} 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. * @return {Array} * An array of string selectors. */ CssLogic.getSelectors = function (domRule) { let selectors = []; let len = domUtils.getSelectorCount(domRule); for (let i = 0; i < len; i++) { let text = domUtils.getSelectorText(domRule, i); selectors.push(text); } 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 ':before', ':after', or null. */ CssLogic.getBindingElementAndPseudo = function (node) { let bindingElement = node; let pseudo = null; if (node.nodeName == "_moz_generated_content_before") { bindingElement = node.parentNode; pseudo = ":before"; } else if (node.nodeName == "_moz_generated_content_after") { bindingElement = node.parentNode; pseudo = ":after"; } return { bindingElement: bindingElement, pseudo: pseudo }; }; /** * 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.ownerDocument || !node.ownerDocument.defaultView) { return null; } let {bindingElement, pseudo} = CssLogic.getBindingElementAndPseudo(node); return node.ownerDocument.defaultView.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) { let href = sheet.href; if (!href) { href = sheet.ownerNode.ownerDocument.location; } return href; }; /** * Find the position of [element] in [nodeList]. * @returns an index of the match, or -1 if there is no match */ function positionInNodeList(element, nodeList) { for (let i = 0; i < nodeList.length; i++) { if (element === nodeList[i]) { return i; } } return -1; } /** * Find a unique CSS selector for a given element * @returns a string such that ele.ownerDocument.querySelector(reply) === ele * and ele.ownerDocument.querySelectorAll(reply).length === 1 */ CssLogic.findCssSelector = function (ele) { ele = getRootBindingParent(ele); let document = ele.ownerDocument; if (!document || !document.contains(ele)) { throw new Error("findCssSelector received element not inside document"); } // document.querySelectorAll("#id") returns multiple if elements share an ID if (ele.id && document.querySelectorAll("#" + CSS.escape(ele.id)).length === 1) { return "#" + CSS.escape(ele.id); } // Inherently unique by tag name let tagName = ele.localName; if (tagName === "html") { return "html"; } if (tagName === "head") { return "head"; } if (tagName === "body") { return "body"; } // We might be able to find a unique class name let selector, index, matches; if (ele.classList.length > 0) { for (let i = 0; i < ele.classList.length; i++) { // Is this className unique by itself? selector = "." + CSS.escape(ele.classList.item(i)); matches = document.querySelectorAll(selector); if (matches.length === 1) { return selector; } // Maybe it's unique with a tag name? selector = tagName + selector; matches = document.querySelectorAll(selector); if (matches.length === 1) { return selector; } // Maybe it's unique using a tag name and nth-child index = positionInNodeList(ele, ele.parentNode.children) + 1; selector = selector + ":nth-child(" + index + ")"; matches = document.querySelectorAll(selector); if (matches.length === 1) { return selector; } } } // Not unique enough yet. As long as it's not a child of the document, // continue recursing up until it is unique enough. if (ele.parentNode !== document) { index = positionInNodeList(ele, ele.parentNode.children) + 1; selector = CssLogic.findCssSelector(ele.parentNode) + " > " + tagName + ":nth-child(" + index + ")"; } return selector; }; /** * Get the full CSS path for a given element. * @returns a string that can be used as a CSS selector for the element. It might not * match the element uniquely. It does however, represent the full path from the root * node to the element. */ CssLogic.getCssPath = function (ele) { ele = getRootBindingParent(ele); const document = ele.ownerDocument; if (!document || !document.contains(ele)) { throw new Error("getCssPath received element not inside document"); } const getElementSelector = element => { if (!element.localName) { return ""; } let label = element.nodeName == element.nodeName.toUpperCase() ? element.localName.toLowerCase() : element.localName; if (element.id) { label += "#" + element.id; } if (element.classList) { for (let cl of element.classList) { label += "." + cl; } } return label; }; let paths = []; while (ele) { if (!ele || ele.nodeType !== Node.ELEMENT_NODE) { break; } paths.splice(0, 0, getElementSelector(ele)); ele = ele.parentNode; } return paths.length ? paths.join(" ") : ""; } /** * 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. */ function CssSheet(cssLogic, domSheet, index) { this._cssLogic = cssLogic; this.domSheet = domSheet; this.index = this.contentSheet ? 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; } CssSheet.prototype = { _passId: null, _contentSheet: null, /** * Tells if the stylesheet is provided by the browser or not. * * @return {boolean} false if this is a browser-provided stylesheet, or true * otherwise. */ get contentSheet() { if (this._contentSheet === null) { this._contentSheet = isContentStylesheet(this.domSheet); } return this._contentSheet; }, /** * Tells 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; let filter = this._cssLogic.sourceFilter; if (filter === FILTER.USER && !this.contentSheet) { 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 nsIDOMCSSRule objects in this stylesheet. */ get ruleCount() { return this._ruleCount > -1 ? this._ruleCount : this.domSheet.cssRules.length; }, /** * 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: function (domRule) { let cacheId = domRule.type + domRule.selectorText; let rule = null; let ruleFound = false; if (cacheId in this._rules) { for (let i = 0, rulesLen = this._rules[cacheId].length; i < rulesLen; i++) { rule = this._rules[cacheId][i]; 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: function () { return "CssSheet[" + this.shortSource + "]"; } }; /** * Information about a single CSSStyleRule. * * @param {CSSSheet|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 */ function CssRule(cssSheet, domRule, element) { this._cssSheet = cssSheet; this.domRule = domRule; let parentRule = domRule.parentRule; if (parentRule && parentRule.type == CSSRule.MEDIA_RULE) { this.mediaText = parentRule.media.mediaText; } if (this._cssSheet) { // parse domRule.selectorText on call to this.selectors this._selectors = null; this.line = domUtils.getRuleLine(this.domRule); this.source = this._cssSheet.shortSource + ":" + this.line; if (this.mediaText) { this.source += " @media " + this.mediaText; } this.href = this._cssSheet.href; this.contentRule = this._cssSheet.contentSheet; } else if (element) { this._selectors = [ new CssSelector(this, "@element.style", 0) ]; this.line = -1; this.source = l10n("rule.sourceElement"); this.href = "#"; this.contentRule = true; this.sourceElement = element; } } CssRule.prototype = { _passId: null, mediaText: "", get isMediaRule() { return !!this.mediaText; }, /** * 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: function (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: function (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; } let 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: function () { return "[CssRule " + this.domRule.selectorText + "]"; }, }; /** * 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. */ function CssSelector(cssRule, selector, index) { this.cssRule = cssRule; this.text = selector; this.elementStyle = this.text == "@element.style"; this._specificity = null; this.selectorIndex = index; } exports.CssSelector = CssSelector; CssSelector.prototype = { _matchId: null, /** * Retrieve the CssSelector source, which is the source of the CssSheet owning * the selector. * * @return {string} the selector source. */ get source() { return this.cssRule.source; }, /** * 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 a browser-provided stylesheet. * * @return {boolean} true if the selector comes from a content-provided * stylesheet, or false otherwise. */ get contentRule() { return this.cssRule.contentRule; }, /** * 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 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.elementStyle) { // 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 0x01000000, just return the constant // directly. @see http://www.w3.org/TR/CSS2/cascade.html#specificity return 0x01000000; } if (this._specificity) { return this._specificity; } this._specificity = domUtils.getSpecificity(this.cssRule.domRule, this.selectorIndex); return this._specificity; }, toString: function () { return this.text; }, }; /** * 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 * @param {function} isInherited A function that determines if the CSS property * is inherited. * @constructor */ function CssPropertyInfo(cssLogic, property, isInherited) { this._cssLogic = cssLogic; this.property = property; this._value = ""; this._isInherited = isInherited; // 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; } CssPropertyInfo.prototype = { /** * 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: function () { 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(function (selectorInfo1, selectorInfo2) { if (selectorInfo1.status > selectorInfo2.status) { return -1; } else if (selectorInfo2.status > selectorInfo1.status) { return 1; } return selectorInfo1.compareTo(selectorInfo2); }); // Now we know which of the matches is best, we can mark it BEST_MATCH. if (this._matchedSelectors.length > 0 && 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. */ _processMatchedSelector: function (selector, status) { let cssRule = selector.cssRule; let value = cssRule.getPropertyValue(this.property); if (value && (status == STATUS.MATCHED || (status == STATUS.PARENT_MATCH && this._isInherited(this.property)))) { let selectorInfo = new CssSelectorInfo(selector, this.property, value, status); this._matchedSelectors.push(selectorInfo); } }, /** * Refilter the matched selectors array when the CssLogic.sourceFilter * changes. This allows for quick filter changes. * @private */ _refilterSelectors: function () { let passId = ++this._cssLogic._passId; let iterator = function (selectorInfo) { let cssRule = selectorInfo.selector.cssRule; if (cssRule._passId != passId) { cssRule._passId = passId; } }; if (this._matchedSelectors) { this._matchedSelectors.forEach(iterator); } this.needRefilter = false; }, toString: function () { return "CssPropertyInfo[" + this.property + "]"; }, }; /** * 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. * @constructor */ function CssSelectorInfo(selector, property, value, status) { this.selector = selector; this.property = property; this.status = status; this.value = value; let priority = this.selector.cssRule.getPropertyPriority(this.property); this.important = (priority === "important"); } CssSelectorInfo.prototype = { /** * Retrieve the CssSelector source, which is the source of the CssSheet owning * the selector. * * @return {string} the selector source. */ get source() { return this.selector.source; }, /** * 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 elementStyle() { return this.selector.elementStyle; }, /** * 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; }, /** * 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 contentRule() { return this.selector.contentRule; }, /** * Compare the current CssSelectorInfo instance to another instance, based on * specificity information. * * @param {CssSelectorInfo} that The instance to compare ourselves against. * @return number -1, 0, 1 depending on how that compares with this. */ compareTo: function (that) { if (!this.contentRule && that.contentRule) { return 1; } if (this.contentRule && !that.contentRule) { return -1; } if (this.elementStyle && !that.elementStyle) { if (!this.important && that.important) { return 1; } return -1; } if (!this.elementStyle && that.elementStyle) { if (this.important && !that.important) { return -1; } return 1; } if (this.important && !that.important) { return -1; } if (that.important && !this.important) { return 1; } if (this.specificity > that.specificity) { return -1; } if (that.specificity > this.specificity) { return 1; } if (this.sheetIndex > that.sheetIndex) { return -1; } if (that.sheetIndex > this.sheetIndex) { return 1; } if (this.ruleLine > that.ruleLine) { return -1; } if (that.ruleLine > this.ruleLine) { return 1; } return 0; }, toString: function () { return this.selector + " -> " + this.value; }, }; DevToolsUtils.defineLazyGetter(this, "domUtils", function () { return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); });