michael@0: /* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /* michael@0: * About the objects defined in this file: michael@0: * - CssLogic contains style information about a view context. It provides michael@0: * access to 2 sets of objects: Css[Sheet|Rule|Selector] provide access to michael@0: * information that does not change when the selected element changes while michael@0: * Css[Property|Selector]Info provide information that is dependent on the michael@0: * selected element. michael@0: * Its key methods are highlight(), getPropertyInfo() and forEachSheet(), etc michael@0: * It also contains a number of static methods for l10n, naming, etc michael@0: * michael@0: * - CssSheet provides a more useful API to a DOM CSSSheet for our purposes, michael@0: * including shortSource and href. michael@0: * - CssRule a more useful API to a nsIDOMCSSRule including access to the group michael@0: * of CssSelectors that the rule provides properties for michael@0: * - CssSelector A single selector - i.e. not a selector group. In other words michael@0: * a CssSelector does not contain ','. This terminology is different from the michael@0: * standard DOM API, but more inline with the definition in the spec. michael@0: * michael@0: * - CssPropertyInfo contains style information for a single property for the michael@0: * highlighted element. michael@0: * - CssSelectorInfo is a wrapper around CssSelector, which adds sorting with michael@0: * reference to the selected element. michael@0: */ michael@0: michael@0: /** michael@0: * Provide access to the style information in a page. michael@0: * CssLogic uses the standard DOM API, and the Gecko inIDOMUtils API to access michael@0: * styling information in the page, and present this to the user in a way that michael@0: * helps them understand: michael@0: * - why their expectations may not have been fulfilled michael@0: * - how browsers process CSS michael@0: * @constructor michael@0: */ michael@0: michael@0: const {Cc, Ci, Cu} = require("chrome"); michael@0: michael@0: const RX_UNIVERSAL_SELECTOR = /\s*\*\s*/g; michael@0: const RX_NOT = /:not\((.*?)\)/g; michael@0: const RX_PSEUDO_CLASS_OR_ELT = /(:[\w-]+\().*?\)/g; michael@0: const RX_CONNECTORS = /\s*[\s>+~]\s*/g; michael@0: const RX_ID = /\s*#\w+\s*/g; michael@0: const RX_CLASS_OR_ATTRIBUTE = /\s*(?:\.\w+|\[.+?\])\s*/g; michael@0: const RX_PSEUDO = /\s*:?:([\w-]+)(\(?\)?)\s*/g; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: function CssLogic() michael@0: { michael@0: // The cache of examined CSS properties. michael@0: _propertyInfos: {}; michael@0: } michael@0: michael@0: exports.CssLogic = CssLogic; michael@0: michael@0: /** michael@0: * Special values for filter, in addition to an href these values can be used michael@0: */ michael@0: CssLogic.FILTER = { michael@0: USER: "user", // show properties for all user style sheets. michael@0: UA: "ua", // USER, plus user-agent (i.e. browser) style sheets michael@0: }; michael@0: michael@0: /** michael@0: * Known media values. To distinguish "all" stylesheets (above) from "all" media michael@0: * The full list includes braille, embossed, handheld, print, projection, michael@0: * speech, tty, and tv, but this is only a hack because these are not defined michael@0: * in the DOM at all. michael@0: * @see http://www.w3.org/TR/CSS21/media.html#media-types michael@0: */ michael@0: CssLogic.MEDIA = { michael@0: ALL: "all", michael@0: SCREEN: "screen", michael@0: }; michael@0: michael@0: /** michael@0: * Each rule has a status, the bigger the number, the better placed it is to michael@0: * provide styling information. michael@0: * michael@0: * These statuses are localized inside the styleinspector.properties string bundle. michael@0: * @see csshtmltree.js RuleView._cacheStatusNames() michael@0: */ michael@0: CssLogic.STATUS = { michael@0: BEST: 3, michael@0: MATCHED: 2, michael@0: PARENT_MATCH: 1, michael@0: UNMATCHED: 0, michael@0: UNKNOWN: -1, michael@0: }; michael@0: michael@0: CssLogic.prototype = { michael@0: // Both setup by highlight(). michael@0: viewedElement: null, michael@0: viewedDocument: null, michael@0: michael@0: // The cache of the known sheets. michael@0: _sheets: null, michael@0: michael@0: // Have the sheets been cached? michael@0: _sheetsCached: false, michael@0: michael@0: // The total number of rules, in all stylesheets, after filtering. michael@0: _ruleCount: 0, michael@0: michael@0: // The computed styles for the viewedElement. michael@0: _computedStyle: null, michael@0: michael@0: // Source filter. Only display properties coming from the given source michael@0: _sourceFilter: CssLogic.FILTER.USER, michael@0: michael@0: // Used for tracking unique CssSheet/CssRule/CssSelector objects, in a run of michael@0: // processMatchedSelectors(). michael@0: _passId: 0, michael@0: michael@0: // Used for tracking matched CssSelector objects. michael@0: _matchId: 0, michael@0: michael@0: _matchedRules: null, michael@0: _matchedSelectors: null, michael@0: michael@0: /** michael@0: * Reset various properties michael@0: */ michael@0: reset: function CssLogic_reset() michael@0: { michael@0: this._propertyInfos = {}; michael@0: this._ruleCount = 0; michael@0: this._sheetIndex = 0; michael@0: this._sheets = {}; michael@0: this._sheetsCached = false; michael@0: this._matchedRules = null; michael@0: this._matchedSelectors = null; michael@0: }, michael@0: michael@0: /** michael@0: * Focus on a new element - remove the style caches. michael@0: * michael@0: * @param {nsIDOMElement} aViewedElement the element the user has highlighted michael@0: * in the Inspector. michael@0: */ michael@0: highlight: function CssLogic_highlight(aViewedElement) michael@0: { michael@0: if (!aViewedElement) { michael@0: this.viewedElement = null; michael@0: this.viewedDocument = null; michael@0: this._computedStyle = null; michael@0: this.reset(); michael@0: return; michael@0: } michael@0: michael@0: this.viewedElement = aViewedElement; michael@0: michael@0: let doc = this.viewedElement.ownerDocument; michael@0: if (doc != this.viewedDocument) { michael@0: // New document: clear/rebuild the cache. michael@0: this.viewedDocument = doc; michael@0: michael@0: // Hunt down top level stylesheets, and cache them. michael@0: this._cacheSheets(); michael@0: } else { michael@0: // Clear cached data in the CssPropertyInfo objects. michael@0: this._propertyInfos = {}; michael@0: } michael@0: michael@0: this._matchedRules = null; michael@0: this._matchedSelectors = null; michael@0: let win = this.viewedDocument.defaultView; michael@0: this._computedStyle = win.getComputedStyle(this.viewedElement, ""); michael@0: }, michael@0: michael@0: /** michael@0: * Get the source filter. michael@0: * @returns {string} The source filter being used. michael@0: */ michael@0: get sourceFilter() { michael@0: return this._sourceFilter; michael@0: }, michael@0: michael@0: /** michael@0: * Source filter. Only display properties coming from the given source (web michael@0: * address). Note that in order to avoid information overload we DO NOT show michael@0: * unmatched system rules. michael@0: * @see CssLogic.FILTER.* michael@0: */ michael@0: set sourceFilter(aValue) { michael@0: let oldValue = this._sourceFilter; michael@0: this._sourceFilter = aValue; michael@0: michael@0: let ruleCount = 0; michael@0: michael@0: // Update the CssSheet objects. michael@0: this.forEachSheet(function(aSheet) { michael@0: aSheet._sheetAllowed = -1; michael@0: if (aSheet.contentSheet && aSheet.sheetAllowed) { michael@0: ruleCount += aSheet.ruleCount; michael@0: } michael@0: }, this); michael@0: michael@0: this._ruleCount = ruleCount; michael@0: michael@0: // Full update is needed because the this.processMatchedSelectors() method michael@0: // skips UA stylesheets if the filter does not allow such sheets. michael@0: let needFullUpdate = (oldValue == CssLogic.FILTER.UA || michael@0: aValue == CssLogic.FILTER.UA); michael@0: michael@0: if (needFullUpdate) { michael@0: this._matchedRules = null; michael@0: this._matchedSelectors = null; michael@0: this._propertyInfos = {}; michael@0: } else { michael@0: // Update the CssPropertyInfo objects. michael@0: for each (let propertyInfo in this._propertyInfos) { michael@0: propertyInfo.needRefilter = true; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Return a CssPropertyInfo data structure for the currently viewed element michael@0: * and the specified CSS property. If there is no currently viewed element we michael@0: * return an empty object. michael@0: * michael@0: * @param {string} aProperty The CSS property to look for. michael@0: * @return {CssPropertyInfo} a CssPropertyInfo structure for the given michael@0: * property. michael@0: */ michael@0: getPropertyInfo: function CssLogic_getPropertyInfo(aProperty) michael@0: { michael@0: if (!this.viewedElement) { michael@0: return {}; michael@0: } michael@0: michael@0: let info = this._propertyInfos[aProperty]; michael@0: if (!info) { michael@0: info = new CssPropertyInfo(this, aProperty); michael@0: this._propertyInfos[aProperty] = info; michael@0: } michael@0: michael@0: return info; michael@0: }, michael@0: michael@0: /** michael@0: * Cache all the stylesheets in the inspected document michael@0: * @private michael@0: */ michael@0: _cacheSheets: function CssLogic_cacheSheets() michael@0: { michael@0: this._passId++; michael@0: this.reset(); michael@0: michael@0: // styleSheets isn't an array, but forEach can work on it anyway michael@0: Array.prototype.forEach.call(this.viewedDocument.styleSheets, michael@0: this._cacheSheet, this); michael@0: michael@0: this._sheetsCached = true; michael@0: }, michael@0: michael@0: /** michael@0: * Cache a stylesheet if it falls within the requirements: if it's enabled, michael@0: * and if the @media is allowed. This method also walks through the stylesheet michael@0: * cssRules to find @imported rules, to cache the stylesheets of those rules michael@0: * as well. michael@0: * michael@0: * @private michael@0: * @param {CSSStyleSheet} aDomSheet the CSSStyleSheet object to cache. michael@0: */ michael@0: _cacheSheet: function CssLogic_cacheSheet(aDomSheet) michael@0: { michael@0: if (aDomSheet.disabled) { michael@0: return; michael@0: } michael@0: michael@0: // Only work with stylesheets that have their media allowed. michael@0: if (!this.mediaMatches(aDomSheet)) { michael@0: return; michael@0: } michael@0: michael@0: // Cache the sheet. michael@0: let cssSheet = this.getSheet(aDomSheet, this._sheetIndex++); michael@0: if (cssSheet._passId != this._passId) { michael@0: cssSheet._passId = this._passId; michael@0: michael@0: // Find import rules. michael@0: Array.prototype.forEach.call(aDomSheet.cssRules, function(aDomRule) { michael@0: if (aDomRule.type == Ci.nsIDOMCSSRule.IMPORT_RULE && aDomRule.styleSheet && michael@0: this.mediaMatches(aDomRule)) { michael@0: this._cacheSheet(aDomRule.styleSheet); michael@0: } michael@0: }, this); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Retrieve the list of stylesheets in the document. michael@0: * michael@0: * @return {array} the list of stylesheets in the document. michael@0: */ michael@0: get sheets() michael@0: { michael@0: if (!this._sheetsCached) { michael@0: this._cacheSheets(); michael@0: } michael@0: michael@0: let sheets = []; michael@0: this.forEachSheet(function (aSheet) { michael@0: if (aSheet.contentSheet) { michael@0: sheets.push(aSheet); michael@0: } michael@0: }, this); michael@0: michael@0: return sheets; michael@0: }, michael@0: michael@0: /** michael@0: * Retrieve a CssSheet object for a given a CSSStyleSheet object. If the michael@0: * stylesheet is already cached, you get the existing CssSheet object, michael@0: * otherwise the new CSSStyleSheet object is cached. michael@0: * michael@0: * @param {CSSStyleSheet} aDomSheet the CSSStyleSheet object you want. michael@0: * @param {number} aIndex the index, within the document, of the stylesheet. michael@0: * michael@0: * @return {CssSheet} the CssSheet object for the given CSSStyleSheet object. michael@0: */ michael@0: getSheet: function CL_getSheet(aDomSheet, aIndex) michael@0: { michael@0: let cacheId = ""; michael@0: michael@0: if (aDomSheet.href) { michael@0: cacheId = aDomSheet.href; michael@0: } else if (aDomSheet.ownerNode && aDomSheet.ownerNode.ownerDocument) { michael@0: cacheId = aDomSheet.ownerNode.ownerDocument.location; michael@0: } michael@0: michael@0: let sheet = null; michael@0: let sheetFound = false; michael@0: michael@0: if (cacheId in this._sheets) { michael@0: for (let i = 0, numSheets = this._sheets[cacheId].length; i < numSheets; i++) { michael@0: sheet = this._sheets[cacheId][i]; michael@0: if (sheet.domSheet === aDomSheet) { michael@0: if (aIndex != -1) { michael@0: sheet.index = aIndex; michael@0: } michael@0: sheetFound = true; michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (!sheetFound) { michael@0: if (!(cacheId in this._sheets)) { michael@0: this._sheets[cacheId] = []; michael@0: } michael@0: michael@0: sheet = new CssSheet(this, aDomSheet, aIndex); michael@0: if (sheet.sheetAllowed && sheet.contentSheet) { michael@0: this._ruleCount += sheet.ruleCount; michael@0: } michael@0: michael@0: this._sheets[cacheId].push(sheet); michael@0: } michael@0: michael@0: return sheet; michael@0: }, michael@0: michael@0: /** michael@0: * Process each cached stylesheet in the document using your callback. michael@0: * michael@0: * @param {function} aCallback the function you want executed for each of the michael@0: * CssSheet objects cached. michael@0: * @param {object} aScope the scope you want for the callback function. aScope michael@0: * will be the this object when aCallback executes. michael@0: */ michael@0: forEachSheet: function CssLogic_forEachSheet(aCallback, aScope) michael@0: { michael@0: for each (let sheets in this._sheets) { michael@0: for (let i = 0; i < sheets.length; i ++) { michael@0: // We take this as an opportunity to clean dead sheets michael@0: try { michael@0: let sheet = sheets[i]; michael@0: sheet.domSheet; // If accessing domSheet raises an exception, then the michael@0: // style sheet is a dead object michael@0: aCallback.call(aScope, sheet, i, sheets); michael@0: } catch (e) { michael@0: sheets.splice(i, 1); michael@0: i --; michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Process *some* cached stylesheets in the document using your callback. The michael@0: * callback function should return true in order to halt processing. michael@0: * michael@0: * @param {function} aCallback the function you want executed for some of the michael@0: * CssSheet objects cached. michael@0: * @param {object} aScope the scope you want for the callback function. aScope michael@0: * will be the this object when aCallback executes. michael@0: * @return {Boolean} true if aCallback returns true during any iteration, michael@0: * otherwise false is returned. michael@0: */ michael@0: forSomeSheets: function CssLogic_forSomeSheets(aCallback, aScope) michael@0: { michael@0: for each (let sheets in this._sheets) { michael@0: if (sheets.some(aCallback, aScope)) { michael@0: return true; michael@0: } michael@0: } michael@0: return false; michael@0: }, michael@0: michael@0: /** michael@0: * Get the number nsIDOMCSSRule objects in the document, counted from all of michael@0: * the stylesheets. System sheets are excluded. If a filter is active, this michael@0: * tells only the number of nsIDOMCSSRule objects inside the selected michael@0: * CSSStyleSheet. michael@0: * michael@0: * WARNING: This only provides an estimate of the rule count, and the results michael@0: * could change at a later date. Todo remove this michael@0: * michael@0: * @return {number} the number of nsIDOMCSSRule (all rules). michael@0: */ michael@0: get ruleCount() michael@0: { michael@0: if (!this._sheetsCached) { michael@0: this._cacheSheets(); michael@0: } michael@0: michael@0: return this._ruleCount; michael@0: }, michael@0: michael@0: /** michael@0: * Process the CssSelector objects that match the highlighted element and its michael@0: * parent elements. aScope.aCallback() is executed for each CssSelector michael@0: * object, being passed the CssSelector object and the match status. michael@0: * michael@0: * This method also includes all of the element.style properties, for each michael@0: * highlighted element parent and for the highlighted element itself. michael@0: * michael@0: * Note that the matched selectors are cached, such that next time your michael@0: * callback is invoked for the cached list of CssSelector objects. michael@0: * michael@0: * @param {function} aCallback the function you want to execute for each of michael@0: * the matched selectors. michael@0: * @param {object} aScope the scope you want for the callback function. aScope michael@0: * will be the this object when aCallback executes. michael@0: */ michael@0: processMatchedSelectors: function CL_processMatchedSelectors(aCallback, aScope) michael@0: { michael@0: if (this._matchedSelectors) { michael@0: if (aCallback) { michael@0: this._passId++; michael@0: this._matchedSelectors.forEach(function(aValue) { michael@0: aCallback.call(aScope, aValue[0], aValue[1]); michael@0: aValue[0].cssRule._passId = this._passId; michael@0: }, this); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: if (!this._matchedRules) { michael@0: this._buildMatchedRules(); michael@0: } michael@0: michael@0: this._matchedSelectors = []; michael@0: this._passId++; michael@0: michael@0: for (let i = 0; i < this._matchedRules.length; i++) { michael@0: let rule = this._matchedRules[i][0]; michael@0: let status = this._matchedRules[i][1]; michael@0: michael@0: rule.selectors.forEach(function (aSelector) { michael@0: if (aSelector._matchId !== this._matchId && michael@0: (aSelector.elementStyle || michael@0: this.selectorMatchesElement(rule.domRule, aSelector.selectorIndex))) { michael@0: michael@0: aSelector._matchId = this._matchId; michael@0: this._matchedSelectors.push([ aSelector, status ]); michael@0: if (aCallback) { michael@0: aCallback.call(aScope, aSelector, status); michael@0: } michael@0: } michael@0: }, this); michael@0: michael@0: rule._passId = this._passId; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Check if the given selector matches the highlighted element or any of its michael@0: * parents. michael@0: * michael@0: * @private michael@0: * @param {DOMRule} domRule michael@0: * The DOM Rule containing the selector. michael@0: * @param {Number} idx michael@0: * The index of the selector within the DOMRule. michael@0: * @return {boolean} michael@0: * true if the given selector matches the highlighted element or any michael@0: * of its parents, otherwise false is returned. michael@0: */ michael@0: selectorMatchesElement: function CL_selectorMatchesElement2(domRule, idx) michael@0: { michael@0: let element = this.viewedElement; michael@0: do { michael@0: if (domUtils.selectorMatchesElement(element, domRule, idx)) { michael@0: return true; michael@0: } michael@0: } while ((element = element.parentNode) && michael@0: element.nodeType === Ci.nsIDOMNode.ELEMENT_NODE); michael@0: michael@0: return false; michael@0: }, michael@0: michael@0: /** michael@0: * Check if the highlighted element or it's parents have matched selectors. michael@0: * michael@0: * @param {array} aProperties The list of properties you want to check if they michael@0: * have matched selectors or not. michael@0: * @return {object} An object that tells for each property if it has matched michael@0: * selectors or not. Object keys are property names and values are booleans. michael@0: */ michael@0: hasMatchedSelectors: function CL_hasMatchedSelectors(aProperties) michael@0: { michael@0: if (!this._matchedRules) { michael@0: this._buildMatchedRules(); michael@0: } michael@0: michael@0: let result = {}; michael@0: michael@0: this._matchedRules.some(function(aValue) { michael@0: let rule = aValue[0]; michael@0: let status = aValue[1]; michael@0: aProperties = aProperties.filter(function(aProperty) { michael@0: // We just need to find if a rule has this property while it matches michael@0: // the viewedElement (or its parents). michael@0: if (rule.getPropertyValue(aProperty) && michael@0: (status == CssLogic.STATUS.MATCHED || michael@0: (status == CssLogic.STATUS.PARENT_MATCH && michael@0: domUtils.isInheritedProperty(aProperty)))) { michael@0: result[aProperty] = true; michael@0: return false; michael@0: } michael@0: return true; // Keep the property for the next rule. michael@0: }.bind(this)); michael@0: return aProperties.length == 0; michael@0: }, this); michael@0: michael@0: return result; michael@0: }, michael@0: michael@0: /** michael@0: * Build the array of matched rules for the currently highlighted element. michael@0: * The array will hold rules that match the viewedElement and its parents. michael@0: * michael@0: * @private michael@0: */ michael@0: _buildMatchedRules: function CL__buildMatchedRules() michael@0: { michael@0: let domRules; michael@0: let element = this.viewedElement; michael@0: let filter = this.sourceFilter; michael@0: let sheetIndex = 0; michael@0: michael@0: this._matchId++; michael@0: this._passId++; michael@0: this._matchedRules = []; michael@0: michael@0: if (!element) { michael@0: return; michael@0: } michael@0: michael@0: do { michael@0: let status = this.viewedElement === element ? michael@0: CssLogic.STATUS.MATCHED : CssLogic.STATUS.PARENT_MATCH; michael@0: michael@0: try { michael@0: domRules = domUtils.getCSSStyleRules(element); michael@0: } catch (ex) { michael@0: Services.console. michael@0: logStringMessage("CL__buildMatchedRules error: " + ex); michael@0: continue; michael@0: } michael@0: michael@0: for (let i = 0, n = domRules.Count(); i < n; i++) { michael@0: let domRule = domRules.GetElementAt(i); michael@0: if (domRule.type !== Ci.nsIDOMCSSRule.STYLE_RULE) { michael@0: continue; michael@0: } michael@0: michael@0: let sheet = this.getSheet(domRule.parentStyleSheet, -1); michael@0: if (sheet._passId !== this._passId) { michael@0: sheet.index = sheetIndex++; michael@0: sheet._passId = this._passId; michael@0: } michael@0: michael@0: if (filter === CssLogic.FILTER.USER && !sheet.contentSheet) { michael@0: continue; michael@0: } michael@0: michael@0: let rule = sheet.getRule(domRule); michael@0: if (rule._passId === this._passId) { michael@0: continue; michael@0: } michael@0: michael@0: rule._matchId = this._matchId; michael@0: rule._passId = this._passId; michael@0: this._matchedRules.push([rule, status]); michael@0: } michael@0: michael@0: michael@0: // Add element.style information. michael@0: if (element.style && element.style.length > 0) { michael@0: let rule = new CssRule(null, { style: element.style }, element); michael@0: rule._matchId = this._matchId; michael@0: rule._passId = this._passId; michael@0: this._matchedRules.push([rule, status]); michael@0: } michael@0: } while ((element = element.parentNode) && michael@0: element.nodeType === Ci.nsIDOMNode.ELEMENT_NODE); michael@0: }, michael@0: michael@0: /** michael@0: * Tells if the given DOM CSS object matches the current view media. michael@0: * michael@0: * @param {object} aDomObject The DOM CSS object to check. michael@0: * @return {boolean} True if the DOM CSS object matches the current view michael@0: * media, or false otherwise. michael@0: */ michael@0: mediaMatches: function CL_mediaMatches(aDomObject) michael@0: { michael@0: let mediaText = aDomObject.media.mediaText; michael@0: return !mediaText || this.viewedDocument.defaultView. michael@0: matchMedia(mediaText).matches; michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * If the element has an id, return '#id'. Otherwise return 'tagname[n]' where michael@0: * n is the index of this element in its siblings. michael@0: *

A technically more 'correct' output from the no-id case might be: michael@0: * 'tagname:nth-of-type(n)' however this is unlikely to be more understood michael@0: * and it is longer. michael@0: * michael@0: * @param {nsIDOMElement} aElement the element for which you want the short name. michael@0: * @return {string} the string to be displayed for aElement. michael@0: */ michael@0: CssLogic.getShortName = function CssLogic_getShortName(aElement) michael@0: { michael@0: if (!aElement) { michael@0: return "null"; michael@0: } michael@0: if (aElement.id) { michael@0: return "#" + aElement.id; michael@0: } michael@0: let priorSiblings = 0; michael@0: let temp = aElement; michael@0: while (temp = temp.previousElementSibling) { michael@0: priorSiblings++; michael@0: } michael@0: return aElement.tagName + "[" + priorSiblings + "]"; michael@0: }; michael@0: michael@0: /** michael@0: * Get an array of short names from the given element to document.body. michael@0: * michael@0: * @param {nsIDOMElement} aElement the element for which you want the array of michael@0: * short names. michael@0: * @return {array} The array of elements. michael@0: *

Each element is an object of the form: michael@0: *

michael@0: */ michael@0: CssLogic.getShortNamePath = function CssLogic_getShortNamePath(aElement) michael@0: { michael@0: let doc = aElement.ownerDocument; michael@0: let reply = []; michael@0: michael@0: if (!aElement) { michael@0: return reply; michael@0: } michael@0: michael@0: // We want to exclude nodes high up the tree (body/html) unless the user michael@0: // has selected that node, in which case we need to report something. michael@0: do { michael@0: reply.unshift({ michael@0: display: CssLogic.getShortName(aElement), michael@0: element: aElement michael@0: }); michael@0: aElement = aElement.parentNode; michael@0: } while (aElement && aElement != doc.body && aElement != doc.head && aElement != doc); michael@0: michael@0: return reply; michael@0: }; michael@0: michael@0: /** michael@0: * Get a string list of selectors for a given DOMRule. michael@0: * michael@0: * @param {DOMRule} aDOMRule michael@0: * The DOMRule to parse. michael@0: * @return {Array} michael@0: * An array of string selectors. michael@0: */ michael@0: CssLogic.getSelectors = function CssLogic_getSelectors(aDOMRule) michael@0: { michael@0: let selectors = []; michael@0: michael@0: let len = domUtils.getSelectorCount(aDOMRule); michael@0: for (let i = 0; i < len; i++) { michael@0: let text = domUtils.getSelectorText(aDOMRule, i); michael@0: selectors.push(text); michael@0: } michael@0: return selectors; michael@0: } michael@0: michael@0: /** michael@0: * Memonized lookup of a l10n string from a string bundle. michael@0: * @param {string} aName The key to lookup. michael@0: * @returns A localized version of the given key. michael@0: */ michael@0: CssLogic.l10n = function(aName) CssLogic._strings.GetStringFromName(aName); michael@0: michael@0: XPCOMUtils.defineLazyGetter(CssLogic, "_strings", function() Services.strings michael@0: .createBundle("chrome://global/locale/devtools/styleinspector.properties")); michael@0: michael@0: /** michael@0: * Is the given property sheet a content stylesheet? michael@0: * michael@0: * @param {CSSStyleSheet} aSheet a stylesheet michael@0: * @return {boolean} true if the given stylesheet is a content stylesheet, michael@0: * false otherwise. michael@0: */ michael@0: CssLogic.isContentStylesheet = function CssLogic_isContentStylesheet(aSheet) michael@0: { michael@0: // All sheets with owner nodes have been included by content. michael@0: if (aSheet.ownerNode) { michael@0: return true; michael@0: } michael@0: michael@0: // If the sheet has a CSSImportRule we need to check the parent stylesheet. michael@0: if (aSheet.ownerRule instanceof Ci.nsIDOMCSSImportRule) { michael@0: return CssLogic.isContentStylesheet(aSheet.parentStyleSheet); michael@0: } michael@0: michael@0: return false; michael@0: }; michael@0: michael@0: /** michael@0: * Get a source for a stylesheet, taking into account embedded stylesheets michael@0: * for which we need to use document.defaultView.location.href rather than michael@0: * sheet.href michael@0: * michael@0: * @param {CSSStyleSheet} aSheet the DOM object for the style sheet. michael@0: * @return {string} the address of the stylesheet. michael@0: */ michael@0: CssLogic.href = function CssLogic_href(aSheet) michael@0: { michael@0: let href = aSheet.href; michael@0: if (!href) { michael@0: href = aSheet.ownerNode.ownerDocument.location; michael@0: } michael@0: michael@0: return href; michael@0: }; michael@0: michael@0: /** michael@0: * Return a shortened version of a style sheet's source. michael@0: * michael@0: * @param {CSSStyleSheet} aSheet the DOM object for the style sheet. michael@0: */ michael@0: CssLogic.shortSource = function CssLogic_shortSource(aSheet) michael@0: { michael@0: // Use a string like "inline" if there is no source href michael@0: if (!aSheet || !aSheet.href) { michael@0: return CssLogic.l10n("rule.sourceInline"); michael@0: } michael@0: michael@0: // We try, in turn, the filename, filePath, query string, whole thing michael@0: let url = {}; michael@0: try { michael@0: url = Services.io.newURI(aSheet.href, null, null); michael@0: url = url.QueryInterface(Ci.nsIURL); michael@0: } catch (ex) { michael@0: // Some UA-provided stylesheets are not valid URLs. michael@0: } michael@0: michael@0: if (url.fileName) { michael@0: return url.fileName; michael@0: } michael@0: michael@0: if (url.filePath) { michael@0: return url.filePath; michael@0: } michael@0: michael@0: if (url.query) { michael@0: return url.query; michael@0: } michael@0: michael@0: let dataUrl = aSheet.href.match(/^(data:[^,]*),/); michael@0: return dataUrl ? dataUrl[1] : aSheet.href; michael@0: } michael@0: michael@0: /** michael@0: * Extract the background image URL (if any) from a property value. michael@0: * Used, for example, for the preview tooltip in the rule view and michael@0: * computed view. michael@0: * michael@0: * @param {String} aProperty michael@0: * @param {String} aSheetHref michael@0: * @return {string} a image URL michael@0: */ michael@0: CssLogic.getBackgroundImageUriFromProperty = function(aProperty, aSheetHref) { michael@0: let startToken = "url(", start = aProperty.indexOf(startToken), end; michael@0: if (start === -1) { michael@0: return null; michael@0: } michael@0: michael@0: aProperty = aProperty.substring(start + startToken.length).trim(); michael@0: let quote = aProperty.substring(0, 1); michael@0: if (quote === "'" || quote === '"') { michael@0: end = aProperty.search(new RegExp(quote + "\\s*\\)")); michael@0: start = 1; michael@0: } else { michael@0: end = aProperty.indexOf(")"); michael@0: start = 0; michael@0: } michael@0: michael@0: let uri = aProperty.substring(start, end).trim(); michael@0: if (aSheetHref) { michael@0: let IOService = Cc["@mozilla.org/network/io-service;1"] michael@0: .getService(Ci.nsIIOService); michael@0: let sheetUri = IOService.newURI(aSheetHref, null, null); michael@0: uri = sheetUri.resolve(uri); michael@0: } michael@0: michael@0: return uri; michael@0: } michael@0: michael@0: /** michael@0: * Find the position of [element] in [nodeList]. michael@0: * @returns an index of the match, or -1 if there is no match michael@0: */ michael@0: function positionInNodeList(element, nodeList) { michael@0: for (var i = 0; i < nodeList.length; i++) { michael@0: if (element === nodeList[i]) { michael@0: return i; michael@0: } michael@0: } michael@0: return -1; michael@0: } michael@0: michael@0: /** michael@0: * Find a unique CSS selector for a given element michael@0: * @returns a string such that ele.ownerDocument.querySelector(reply) === ele michael@0: * and ele.ownerDocument.querySelectorAll(reply).length === 1 michael@0: */ michael@0: CssLogic.findCssSelector = function CssLogic_findCssSelector(ele) { michael@0: var document = ele.ownerDocument; michael@0: if (ele.id && document.getElementById(ele.id) === ele) { michael@0: return '#' + ele.id; michael@0: } michael@0: michael@0: // Inherently unique by tag name michael@0: var tagName = ele.tagName.toLowerCase(); michael@0: if (tagName === 'html') { michael@0: return 'html'; michael@0: } michael@0: if (tagName === 'head') { michael@0: return 'head'; michael@0: } michael@0: if (tagName === 'body') { michael@0: return 'body'; michael@0: } michael@0: michael@0: if (ele.parentNode == null) { michael@0: console.log('danger: ' + tagName); michael@0: } michael@0: michael@0: // We might be able to find a unique class name michael@0: var selector, index, matches; michael@0: if (ele.classList.length > 0) { michael@0: for (var i = 0; i < ele.classList.length; i++) { michael@0: // Is this className unique by itself? michael@0: selector = '.' + ele.classList.item(i); michael@0: matches = document.querySelectorAll(selector); michael@0: if (matches.length === 1) { michael@0: return selector; michael@0: } michael@0: // Maybe it's unique with a tag name? michael@0: selector = tagName + selector; michael@0: matches = document.querySelectorAll(selector); michael@0: if (matches.length === 1) { michael@0: return selector; michael@0: } michael@0: // Maybe it's unique using a tag name and nth-child michael@0: index = positionInNodeList(ele, ele.parentNode.children) + 1; michael@0: selector = selector + ':nth-child(' + index + ')'; michael@0: matches = document.querySelectorAll(selector); michael@0: if (matches.length === 1) { michael@0: return selector; michael@0: } michael@0: } michael@0: } michael@0: michael@0: // So we can be unique w.r.t. our parent, and use recursion michael@0: index = positionInNodeList(ele, ele.parentNode.children) + 1; michael@0: selector = CssLogic_findCssSelector(ele.parentNode) + ' > ' + michael@0: tagName + ':nth-child(' + index + ')'; michael@0: michael@0: return selector; michael@0: }; michael@0: michael@0: /** michael@0: * A safe way to access cached bits of information about a stylesheet. michael@0: * michael@0: * @constructor michael@0: * @param {CssLogic} aCssLogic pointer to the CssLogic instance working with michael@0: * this CssSheet object. michael@0: * @param {CSSStyleSheet} aDomSheet reference to a DOM CSSStyleSheet object. michael@0: * @param {number} aIndex tells the index/position of the stylesheet within the michael@0: * main document. michael@0: */ michael@0: function CssSheet(aCssLogic, aDomSheet, aIndex) michael@0: { michael@0: this._cssLogic = aCssLogic; michael@0: this.domSheet = aDomSheet; michael@0: this.index = this.contentSheet ? aIndex : -100 * aIndex; michael@0: michael@0: // Cache of the sheets href. Cached by the getter. michael@0: this._href = null; michael@0: // Short version of href for use in select boxes etc. Cached by getter. michael@0: this._shortSource = null; michael@0: michael@0: // null for uncached. michael@0: this._sheetAllowed = null; michael@0: michael@0: // Cached CssRules from the given stylesheet. michael@0: this._rules = {}; michael@0: michael@0: this._ruleCount = -1; michael@0: } michael@0: michael@0: CssSheet.prototype = { michael@0: _passId: null, michael@0: _contentSheet: null, michael@0: _mediaMatches: null, michael@0: michael@0: /** michael@0: * Tells if the stylesheet is provided by the browser or not. michael@0: * michael@0: * @return {boolean} false if this is a browser-provided stylesheet, or true michael@0: * otherwise. michael@0: */ michael@0: get contentSheet() michael@0: { michael@0: if (this._contentSheet === null) { michael@0: this._contentSheet = CssLogic.isContentStylesheet(this.domSheet); michael@0: } michael@0: return this._contentSheet; michael@0: }, michael@0: michael@0: /** michael@0: * Tells if the stylesheet is disabled or not. michael@0: * @return {boolean} true if this stylesheet is disabled, or false otherwise. michael@0: */ michael@0: get disabled() michael@0: { michael@0: return this.domSheet.disabled; michael@0: }, michael@0: michael@0: /** michael@0: * Tells if the stylesheet matches the current browser view media. michael@0: * @return {boolean} true if this stylesheet matches the current browser view michael@0: * media, or false otherwise. michael@0: */ michael@0: get mediaMatches() michael@0: { michael@0: if (this._mediaMatches === null) { michael@0: this._mediaMatches = this._cssLogic.mediaMatches(this.domSheet); michael@0: } michael@0: return this._mediaMatches; michael@0: }, michael@0: michael@0: /** michael@0: * Get a source for a stylesheet, using CssLogic.href michael@0: * michael@0: * @return {string} the address of the stylesheet. michael@0: */ michael@0: get href() michael@0: { michael@0: if (this._href) { michael@0: return this._href; michael@0: } michael@0: michael@0: this._href = CssLogic.href(this.domSheet); michael@0: return this._href; michael@0: }, michael@0: michael@0: /** michael@0: * Create a shorthand version of the href of a stylesheet. michael@0: * michael@0: * @return {string} the shorthand source of the stylesheet. michael@0: */ michael@0: get shortSource() michael@0: { michael@0: if (this._shortSource) { michael@0: return this._shortSource; michael@0: } michael@0: michael@0: this._shortSource = CssLogic.shortSource(this.domSheet); michael@0: return this._shortSource; michael@0: }, michael@0: michael@0: /** michael@0: * Tells if the sheet is allowed or not by the current CssLogic.sourceFilter. michael@0: * michael@0: * @return {boolean} true if the stylesheet is allowed by the sourceFilter, or michael@0: * false otherwise. michael@0: */ michael@0: get sheetAllowed() michael@0: { michael@0: if (this._sheetAllowed !== null) { michael@0: return this._sheetAllowed; michael@0: } michael@0: michael@0: this._sheetAllowed = true; michael@0: michael@0: let filter = this._cssLogic.sourceFilter; michael@0: if (filter === CssLogic.FILTER.USER && !this.contentSheet) { michael@0: this._sheetAllowed = false; michael@0: } michael@0: if (filter !== CssLogic.FILTER.USER && filter !== CssLogic.FILTER.UA) { michael@0: this._sheetAllowed = (filter === this.href); michael@0: } michael@0: michael@0: return this._sheetAllowed; michael@0: }, michael@0: michael@0: /** michael@0: * Retrieve the number of rules in this stylesheet. michael@0: * michael@0: * @return {number} the number of nsIDOMCSSRule objects in this stylesheet. michael@0: */ michael@0: get ruleCount() michael@0: { michael@0: return this._ruleCount > -1 ? michael@0: this._ruleCount : michael@0: this.domSheet.cssRules.length; michael@0: }, michael@0: michael@0: /** michael@0: * Retrieve a CssRule object for the given CSSStyleRule. The CssRule object is michael@0: * cached, such that subsequent retrievals return the same CssRule object for michael@0: * the same CSSStyleRule object. michael@0: * michael@0: * @param {CSSStyleRule} aDomRule the CSSStyleRule object for which you want a michael@0: * CssRule object. michael@0: * @return {CssRule} the cached CssRule object for the given CSSStyleRule michael@0: * object. michael@0: */ michael@0: getRule: function CssSheet_getRule(aDomRule) michael@0: { michael@0: let cacheId = aDomRule.type + aDomRule.selectorText; michael@0: michael@0: let rule = null; michael@0: let ruleFound = false; michael@0: michael@0: if (cacheId in this._rules) { michael@0: for (let i = 0, rulesLen = this._rules[cacheId].length; i < rulesLen; i++) { michael@0: rule = this._rules[cacheId][i]; michael@0: if (rule.domRule === aDomRule) { michael@0: ruleFound = true; michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (!ruleFound) { michael@0: if (!(cacheId in this._rules)) { michael@0: this._rules[cacheId] = []; michael@0: } michael@0: michael@0: rule = new CssRule(this, aDomRule); michael@0: this._rules[cacheId].push(rule); michael@0: } michael@0: michael@0: return rule; michael@0: }, michael@0: michael@0: /** michael@0: * Process each rule in this stylesheet using your callback function. Your michael@0: * function receives one argument: the CssRule object for each CSSStyleRule michael@0: * inside the stylesheet. michael@0: * michael@0: * Note that this method also iterates through @media rules inside the michael@0: * stylesheet. michael@0: * michael@0: * @param {function} aCallback the function you want to execute for each of michael@0: * the style rules. michael@0: * @param {object} aScope the scope you want for the callback function. aScope michael@0: * will be the this object when aCallback executes. michael@0: */ michael@0: forEachRule: function CssSheet_forEachRule(aCallback, aScope) michael@0: { michael@0: let ruleCount = 0; michael@0: let domRules = this.domSheet.cssRules; michael@0: michael@0: function _iterator(aDomRule) { michael@0: if (aDomRule.type == Ci.nsIDOMCSSRule.STYLE_RULE) { michael@0: aCallback.call(aScope, this.getRule(aDomRule)); michael@0: ruleCount++; michael@0: } else if (aDomRule.type == Ci.nsIDOMCSSRule.MEDIA_RULE && michael@0: aDomRule.cssRules && this._cssLogic.mediaMatches(aDomRule)) { michael@0: Array.prototype.forEach.call(aDomRule.cssRules, _iterator, this); michael@0: } michael@0: } michael@0: michael@0: Array.prototype.forEach.call(domRules, _iterator, this); michael@0: michael@0: this._ruleCount = ruleCount; michael@0: }, michael@0: michael@0: /** michael@0: * Process *some* rules in this stylesheet using your callback function. Your michael@0: * function receives one argument: the CssRule object for each CSSStyleRule michael@0: * inside the stylesheet. In order to stop processing the callback function michael@0: * needs to return a value. michael@0: * michael@0: * Note that this method also iterates through @media rules inside the michael@0: * stylesheet. michael@0: * michael@0: * @param {function} aCallback the function you want to execute for each of michael@0: * the style rules. michael@0: * @param {object} aScope the scope you want for the callback function. aScope michael@0: * will be the this object when aCallback executes. michael@0: * @return {Boolean} true if aCallback returns true during any iteration, michael@0: * otherwise false is returned. michael@0: */ michael@0: forSomeRules: function CssSheet_forSomeRules(aCallback, aScope) michael@0: { michael@0: let domRules = this.domSheet.cssRules; michael@0: function _iterator(aDomRule) { michael@0: if (aDomRule.type == Ci.nsIDOMCSSRule.STYLE_RULE) { michael@0: return aCallback.call(aScope, this.getRule(aDomRule)); michael@0: } else if (aDomRule.type == Ci.nsIDOMCSSRule.MEDIA_RULE && michael@0: aDomRule.cssRules && this._cssLogic.mediaMatches(aDomRule)) { michael@0: return Array.prototype.some.call(aDomRule.cssRules, _iterator, this); michael@0: } michael@0: } michael@0: return Array.prototype.some.call(domRules, _iterator, this); michael@0: }, michael@0: michael@0: toString: function CssSheet_toString() michael@0: { michael@0: return "CssSheet[" + this.shortSource + "]"; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Information about a single CSSStyleRule. michael@0: * michael@0: * @param {CSSSheet|null} aCssSheet the CssSheet object of the stylesheet that michael@0: * holds the CSSStyleRule. If the rule comes from element.style, set this michael@0: * argument to null. michael@0: * @param {CSSStyleRule|object} aDomRule the DOM CSSStyleRule for which you want michael@0: * to cache data. If the rule comes from element.style, then provide michael@0: * an object of the form: {style: element.style}. michael@0: * @param {Element} [aElement] If the rule comes from element.style, then this michael@0: * argument must point to the element. michael@0: * @constructor michael@0: */ michael@0: function CssRule(aCssSheet, aDomRule, aElement) michael@0: { michael@0: this._cssSheet = aCssSheet; michael@0: this.domRule = aDomRule; michael@0: michael@0: let parentRule = aDomRule.parentRule; michael@0: if (parentRule && parentRule.type == Ci.nsIDOMCSSRule.MEDIA_RULE) { michael@0: this.mediaText = parentRule.media.mediaText; michael@0: } michael@0: michael@0: if (this._cssSheet) { michael@0: // parse domRule.selectorText on call to this.selectors michael@0: this._selectors = null; michael@0: this.line = domUtils.getRuleLine(this.domRule); michael@0: this.source = this._cssSheet.shortSource + ":" + this.line; michael@0: if (this.mediaText) { michael@0: this.source += " @media " + this.mediaText; michael@0: } michael@0: this.href = this._cssSheet.href; michael@0: this.contentRule = this._cssSheet.contentSheet; michael@0: } else if (aElement) { michael@0: this._selectors = [ new CssSelector(this, "@element.style", 0) ]; michael@0: this.line = -1; michael@0: this.source = CssLogic.l10n("rule.sourceElement"); michael@0: this.href = "#"; michael@0: this.contentRule = true; michael@0: this.sourceElement = aElement; michael@0: } michael@0: } michael@0: michael@0: CssRule.prototype = { michael@0: _passId: null, michael@0: michael@0: mediaText: "", michael@0: michael@0: get isMediaRule() michael@0: { michael@0: return !!this.mediaText; michael@0: }, michael@0: michael@0: /** michael@0: * Check if the parent stylesheet is allowed by the CssLogic.sourceFilter. michael@0: * michael@0: * @return {boolean} true if the parent stylesheet is allowed by the current michael@0: * sourceFilter, or false otherwise. michael@0: */ michael@0: get sheetAllowed() michael@0: { michael@0: return this._cssSheet ? this._cssSheet.sheetAllowed : true; michael@0: }, michael@0: michael@0: /** michael@0: * Retrieve the parent stylesheet index/position in the viewed document. michael@0: * michael@0: * @return {number} the parent stylesheet index/position in the viewed michael@0: * document. michael@0: */ michael@0: get sheetIndex() michael@0: { michael@0: return this._cssSheet ? this._cssSheet.index : 0; michael@0: }, michael@0: michael@0: /** michael@0: * Retrieve the style property value from the current CSSStyleRule. michael@0: * michael@0: * @param {string} aProperty the CSS property name for which you want the michael@0: * value. michael@0: * @return {string} the property value. michael@0: */ michael@0: getPropertyValue: function(aProperty) michael@0: { michael@0: return this.domRule.style.getPropertyValue(aProperty); michael@0: }, michael@0: michael@0: /** michael@0: * Retrieve the style property priority from the current CSSStyleRule. michael@0: * michael@0: * @param {string} aProperty the CSS property name for which you want the michael@0: * priority. michael@0: * @return {string} the property priority. michael@0: */ michael@0: getPropertyPriority: function(aProperty) michael@0: { michael@0: return this.domRule.style.getPropertyPriority(aProperty); michael@0: }, michael@0: michael@0: /** michael@0: * Retrieve the list of CssSelector objects for each of the parsed selectors michael@0: * of the current CSSStyleRule. michael@0: * michael@0: * @return {array} the array hold the CssSelector objects. michael@0: */ michael@0: get selectors() michael@0: { michael@0: if (this._selectors) { michael@0: return this._selectors; michael@0: } michael@0: michael@0: // Parse the CSSStyleRule.selectorText string. michael@0: this._selectors = []; michael@0: michael@0: if (!this.domRule.selectorText) { michael@0: return this._selectors; michael@0: } michael@0: michael@0: let selectors = CssLogic.getSelectors(this.domRule); michael@0: michael@0: for (let i = 0, len = selectors.length; i < len; i++) { michael@0: this._selectors.push(new CssSelector(this, selectors[i], i)); michael@0: } michael@0: michael@0: return this._selectors; michael@0: }, michael@0: michael@0: toString: function CssRule_toString() michael@0: { michael@0: return "[CssRule " + this.domRule.selectorText + "]"; michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * The CSS selector class allows us to document the ranking of various CSS michael@0: * selectors. michael@0: * michael@0: * @constructor michael@0: * @param {CssRule} aCssRule the CssRule instance from where the selector comes. michael@0: * @param {string} aSelector The selector that we wish to investigate. michael@0: * @param {Number} aIndex The index of the selector within it's rule. michael@0: */ michael@0: function CssSelector(aCssRule, aSelector, aIndex) michael@0: { michael@0: this.cssRule = aCssRule; michael@0: this.text = aSelector; michael@0: this.elementStyle = this.text == "@element.style"; michael@0: this._specificity = null; michael@0: this.selectorIndex = aIndex; michael@0: } michael@0: michael@0: exports.CssSelector = CssSelector; michael@0: michael@0: CssSelector.prototype = { michael@0: _matchId: null, michael@0: michael@0: /** michael@0: * Retrieve the CssSelector source, which is the source of the CssSheet owning michael@0: * the selector. michael@0: * michael@0: * @return {string} the selector source. michael@0: */ michael@0: get source() michael@0: { michael@0: return this.cssRule.source; michael@0: }, michael@0: michael@0: /** michael@0: * Retrieve the CssSelector source element, which is the source of the CssRule michael@0: * owning the selector. This is only available when the CssSelector comes from michael@0: * an element.style. michael@0: * michael@0: * @return {string} the source element selector. michael@0: */ michael@0: get sourceElement() michael@0: { michael@0: return this.cssRule.sourceElement; michael@0: }, michael@0: michael@0: /** michael@0: * Retrieve the address of the CssSelector. This points to the address of the michael@0: * CssSheet owning this selector. michael@0: * michael@0: * @return {string} the address of the CssSelector. michael@0: */ michael@0: get href() michael@0: { michael@0: return this.cssRule.href; michael@0: }, michael@0: michael@0: /** michael@0: * Check if the selector comes from a browser-provided stylesheet. michael@0: * michael@0: * @return {boolean} true if the selector comes from a content-provided michael@0: * stylesheet, or false otherwise. michael@0: */ michael@0: get contentRule() michael@0: { michael@0: return this.cssRule.contentRule; michael@0: }, michael@0: michael@0: /** michael@0: * Check if the parent stylesheet is allowed by the CssLogic.sourceFilter. michael@0: * michael@0: * @return {boolean} true if the parent stylesheet is allowed by the current michael@0: * sourceFilter, or false otherwise. michael@0: */ michael@0: get sheetAllowed() michael@0: { michael@0: return this.cssRule.sheetAllowed; michael@0: }, michael@0: michael@0: /** michael@0: * Retrieve the parent stylesheet index/position in the viewed document. michael@0: * michael@0: * @return {number} the parent stylesheet index/position in the viewed michael@0: * document. michael@0: */ michael@0: get sheetIndex() michael@0: { michael@0: return this.cssRule.sheetIndex; michael@0: }, michael@0: michael@0: /** michael@0: * Retrieve the line of the parent CSSStyleRule in the parent CSSStyleSheet. michael@0: * michael@0: * @return {number} the line of the parent CSSStyleRule in the parent michael@0: * stylesheet. michael@0: */ michael@0: get ruleLine() michael@0: { michael@0: return this.cssRule.line; michael@0: }, michael@0: michael@0: /** michael@0: * Retrieve the pseudo-elements that we support. This list should match the michael@0: * elements specified in layout/style/nsCSSPseudoElementList.h michael@0: */ michael@0: get pseudoElements() michael@0: { michael@0: if (!CssSelector._pseudoElements) { michael@0: let pseudos = CssSelector._pseudoElements = new Set(); michael@0: pseudos.add("after"); michael@0: pseudos.add("before"); michael@0: pseudos.add("first-letter"); michael@0: pseudos.add("first-line"); michael@0: pseudos.add("selection"); michael@0: pseudos.add("-moz-color-swatch"); michael@0: pseudos.add("-moz-focus-inner"); michael@0: pseudos.add("-moz-focus-outer"); michael@0: pseudos.add("-moz-list-bullet"); michael@0: pseudos.add("-moz-list-number"); michael@0: pseudos.add("-moz-math-anonymous"); michael@0: pseudos.add("-moz-math-stretchy"); michael@0: pseudos.add("-moz-progress-bar"); michael@0: pseudos.add("-moz-selection"); michael@0: } michael@0: return CssSelector._pseudoElements; michael@0: }, michael@0: michael@0: /** michael@0: * Retrieve specificity information for the current selector. michael@0: * michael@0: * @see http://www.w3.org/TR/css3-selectors/#specificity michael@0: * @see http://www.w3.org/TR/CSS2/selector.html michael@0: * michael@0: * @return {Number} The selector's specificity. michael@0: */ michael@0: get specificity() michael@0: { michael@0: if (this._specificity) { michael@0: return this._specificity; michael@0: } michael@0: michael@0: this._specificity = domUtils.getSpecificity(this.cssRule.domRule, michael@0: this.selectorIndex); michael@0: michael@0: return this._specificity; michael@0: }, michael@0: michael@0: toString: function CssSelector_toString() michael@0: { michael@0: return this.text; michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * A cache of information about the matched rules, selectors and values attached michael@0: * to a CSS property, for the highlighted element. michael@0: * michael@0: * The heart of the CssPropertyInfo object is the _findMatchedSelectors() michael@0: * method. This are invoked when the PropertyView tries to access the michael@0: * .matchedSelectors array. michael@0: * Results are cached, for later reuse. michael@0: * michael@0: * @param {CssLogic} aCssLogic Reference to the parent CssLogic instance michael@0: * @param {string} aProperty The CSS property we are gathering information for michael@0: * @constructor michael@0: */ michael@0: function CssPropertyInfo(aCssLogic, aProperty) michael@0: { michael@0: this._cssLogic = aCssLogic; michael@0: this.property = aProperty; michael@0: this._value = ""; michael@0: michael@0: // The number of matched rules holding the this.property style property. michael@0: // Additionally, only rules that come from allowed stylesheets are counted. michael@0: this._matchedRuleCount = 0; michael@0: michael@0: // An array holding CssSelectorInfo objects for each of the matched selectors michael@0: // that are inside a CSS rule. Only rules that hold the this.property are michael@0: // counted. This includes rules that come from filtered stylesheets (those michael@0: // that have sheetAllowed = false). michael@0: this._matchedSelectors = null; michael@0: } michael@0: michael@0: CssPropertyInfo.prototype = { michael@0: /** michael@0: * Retrieve the computed style value for the current property, for the michael@0: * highlighted element. michael@0: * michael@0: * @return {string} the computed style value for the current property, for the michael@0: * highlighted element. michael@0: */ michael@0: get value() michael@0: { michael@0: if (!this._value && this._cssLogic._computedStyle) { michael@0: try { michael@0: this._value = this._cssLogic._computedStyle.getPropertyValue(this.property); michael@0: } catch (ex) { michael@0: Services.console.logStringMessage('Error reading computed style for ' + michael@0: this.property); michael@0: Services.console.logStringMessage(ex); michael@0: } michael@0: } michael@0: return this._value; michael@0: }, michael@0: michael@0: /** michael@0: * Retrieve the number of matched rules holding the this.property style michael@0: * property. Only rules that come from allowed stylesheets are counted. michael@0: * michael@0: * @return {number} the number of matched rules. michael@0: */ michael@0: get matchedRuleCount() michael@0: { michael@0: if (!this._matchedSelectors) { michael@0: this._findMatchedSelectors(); michael@0: } else if (this.needRefilter) { michael@0: this._refilterSelectors(); michael@0: } michael@0: michael@0: return this._matchedRuleCount; michael@0: }, michael@0: michael@0: /** michael@0: * Retrieve the array holding CssSelectorInfo objects for each of the matched michael@0: * selectors, from each of the matched rules. Only selectors coming from michael@0: * allowed stylesheets are included in the array. michael@0: * michael@0: * @return {array} the list of CssSelectorInfo objects of selectors that match michael@0: * the highlighted element and its parents. michael@0: */ michael@0: get matchedSelectors() michael@0: { michael@0: if (!this._matchedSelectors) { michael@0: this._findMatchedSelectors(); michael@0: } else if (this.needRefilter) { michael@0: this._refilterSelectors(); michael@0: } michael@0: michael@0: return this._matchedSelectors; michael@0: }, michael@0: michael@0: /** michael@0: * Find the selectors that match the highlighted element and its parents. michael@0: * Uses CssLogic.processMatchedSelectors() to find the matched selectors, michael@0: * passing in a reference to CssPropertyInfo._processMatchedSelector() to michael@0: * create CssSelectorInfo objects, which we then sort michael@0: * @private michael@0: */ michael@0: _findMatchedSelectors: function CssPropertyInfo_findMatchedSelectors() michael@0: { michael@0: this._matchedSelectors = []; michael@0: this._matchedRuleCount = 0; michael@0: this.needRefilter = false; michael@0: michael@0: this._cssLogic.processMatchedSelectors(this._processMatchedSelector, this); michael@0: michael@0: // Sort the selectors by how well they match the given element. michael@0: this._matchedSelectors.sort(function(aSelectorInfo1, aSelectorInfo2) { michael@0: if (aSelectorInfo1.status > aSelectorInfo2.status) { michael@0: return -1; michael@0: } else if (aSelectorInfo2.status > aSelectorInfo1.status) { michael@0: return 1; michael@0: } else { michael@0: return aSelectorInfo1.compareTo(aSelectorInfo2); michael@0: } michael@0: }); michael@0: michael@0: // Now we know which of the matches is best, we can mark it BEST_MATCH. michael@0: if (this._matchedSelectors.length > 0 && michael@0: this._matchedSelectors[0].status > CssLogic.STATUS.UNMATCHED) { michael@0: this._matchedSelectors[0].status = CssLogic.STATUS.BEST; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Process a matched CssSelector object. michael@0: * michael@0: * @private michael@0: * @param {CssSelector} aSelector the matched CssSelector object. michael@0: * @param {CssLogic.STATUS} aStatus the CssSelector match status. michael@0: */ michael@0: _processMatchedSelector: function CssPropertyInfo_processMatchedSelector(aSelector, aStatus) michael@0: { michael@0: let cssRule = aSelector.cssRule; michael@0: let value = cssRule.getPropertyValue(this.property); michael@0: if (value && michael@0: (aStatus == CssLogic.STATUS.MATCHED || michael@0: (aStatus == CssLogic.STATUS.PARENT_MATCH && michael@0: domUtils.isInheritedProperty(this.property)))) { michael@0: let selectorInfo = new CssSelectorInfo(aSelector, this.property, value, michael@0: aStatus); michael@0: this._matchedSelectors.push(selectorInfo); michael@0: if (this._cssLogic._passId !== cssRule._passId && cssRule.sheetAllowed) { michael@0: this._matchedRuleCount++; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Refilter the matched selectors array when the CssLogic.sourceFilter michael@0: * changes. This allows for quick filter changes. michael@0: * @private michael@0: */ michael@0: _refilterSelectors: function CssPropertyInfo_refilterSelectors() michael@0: { michael@0: let passId = ++this._cssLogic._passId; michael@0: let ruleCount = 0; michael@0: michael@0: let iterator = function(aSelectorInfo) { michael@0: let cssRule = aSelectorInfo.selector.cssRule; michael@0: if (cssRule._passId != passId) { michael@0: if (cssRule.sheetAllowed) { michael@0: ruleCount++; michael@0: } michael@0: cssRule._passId = passId; michael@0: } michael@0: }; michael@0: michael@0: if (this._matchedSelectors) { michael@0: this._matchedSelectors.forEach(iterator); michael@0: this._matchedRuleCount = ruleCount; michael@0: } michael@0: michael@0: this.needRefilter = false; michael@0: }, michael@0: michael@0: toString: function CssPropertyInfo_toString() michael@0: { michael@0: return "CssPropertyInfo[" + this.property + "]"; michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * A class that holds information about a given CssSelector object. michael@0: * michael@0: * Instances of this class are given to CssHtmlTree in the array of matched michael@0: * selectors. Each such object represents a displayable row in the PropertyView michael@0: * objects. The information given by this object blends data coming from the michael@0: * CssSheet, CssRule and from the CssSelector that own this object. michael@0: * michael@0: * @param {CssSelector} aSelector The CssSelector object for which to present information. michael@0: * @param {string} aProperty The property for which information should be retrieved. michael@0: * @param {string} aValue The property value from the CssRule that owns the selector. michael@0: * @param {CssLogic.STATUS} aStatus The selector match status. michael@0: * @constructor michael@0: */ michael@0: function CssSelectorInfo(aSelector, aProperty, aValue, aStatus) michael@0: { michael@0: this.selector = aSelector; michael@0: this.property = aProperty; michael@0: this.status = aStatus; michael@0: this.value = aValue; michael@0: let priority = this.selector.cssRule.getPropertyPriority(this.property); michael@0: this.important = (priority === "important"); michael@0: } michael@0: michael@0: CssSelectorInfo.prototype = { michael@0: /** michael@0: * Retrieve the CssSelector source, which is the source of the CssSheet owning michael@0: * the selector. michael@0: * michael@0: * @return {string} the selector source. michael@0: */ michael@0: get source() michael@0: { michael@0: return this.selector.source; michael@0: }, michael@0: michael@0: /** michael@0: * Retrieve the CssSelector source element, which is the source of the CssRule michael@0: * owning the selector. This is only available when the CssSelector comes from michael@0: * an element.style. michael@0: * michael@0: * @return {string} the source element selector. michael@0: */ michael@0: get sourceElement() michael@0: { michael@0: return this.selector.sourceElement; michael@0: }, michael@0: michael@0: /** michael@0: * Retrieve the address of the CssSelector. This points to the address of the michael@0: * CssSheet owning this selector. michael@0: * michael@0: * @return {string} the address of the CssSelector. michael@0: */ michael@0: get href() michael@0: { michael@0: return this.selector.href; michael@0: }, michael@0: michael@0: /** michael@0: * Check if the CssSelector comes from element.style or not. michael@0: * michael@0: * @return {boolean} true if the CssSelector comes from element.style, or michael@0: * false otherwise. michael@0: */ michael@0: get elementStyle() michael@0: { michael@0: return this.selector.elementStyle; michael@0: }, michael@0: michael@0: /** michael@0: * Retrieve specificity information for the current selector. michael@0: * michael@0: * @return {object} an object holding specificity information for the current michael@0: * selector. michael@0: */ michael@0: get specificity() michael@0: { michael@0: return this.selector.specificity; michael@0: }, michael@0: michael@0: /** michael@0: * Retrieve the parent stylesheet index/position in the viewed document. michael@0: * michael@0: * @return {number} the parent stylesheet index/position in the viewed michael@0: * document. michael@0: */ michael@0: get sheetIndex() michael@0: { michael@0: return this.selector.sheetIndex; michael@0: }, michael@0: michael@0: /** michael@0: * Check if the parent stylesheet is allowed by the CssLogic.sourceFilter. michael@0: * michael@0: * @return {boolean} true if the parent stylesheet is allowed by the current michael@0: * sourceFilter, or false otherwise. michael@0: */ michael@0: get sheetAllowed() michael@0: { michael@0: return this.selector.sheetAllowed; michael@0: }, michael@0: michael@0: /** michael@0: * Retrieve the line of the parent CSSStyleRule in the parent CSSStyleSheet. michael@0: * michael@0: * @return {number} the line of the parent CSSStyleRule in the parent michael@0: * stylesheet. michael@0: */ michael@0: get ruleLine() michael@0: { michael@0: return this.selector.ruleLine; michael@0: }, michael@0: michael@0: /** michael@0: * Check if the selector comes from a browser-provided stylesheet. michael@0: * michael@0: * @return {boolean} true if the selector comes from a browser-provided michael@0: * stylesheet, or false otherwise. michael@0: */ michael@0: get contentRule() michael@0: { michael@0: return this.selector.contentRule; michael@0: }, michael@0: michael@0: /** michael@0: * Compare the current CssSelectorInfo instance to another instance, based on michael@0: * specificity information. michael@0: * michael@0: * @param {CssSelectorInfo} aThat The instance to compare ourselves against. michael@0: * @return number -1, 0, 1 depending on how aThat compares with this. michael@0: */ michael@0: compareTo: function CssSelectorInfo_compareTo(aThat) michael@0: { michael@0: if (!this.contentRule && aThat.contentRule) return 1; michael@0: if (this.contentRule && !aThat.contentRule) return -1; michael@0: michael@0: if (this.elementStyle && !aThat.elementStyle) { michael@0: if (!this.important && aThat.important) return 1; michael@0: else return -1; michael@0: } michael@0: michael@0: if (!this.elementStyle && aThat.elementStyle) { michael@0: if (this.important && !aThat.important) return -1; michael@0: else return 1; michael@0: } michael@0: michael@0: if (this.important && !aThat.important) return -1; michael@0: if (aThat.important && !this.important) return 1; michael@0: michael@0: if (this.specificity > aThat.specificity) return -1; michael@0: if (aThat.specificity > this.specificity) return 1; michael@0: michael@0: if (this.sheetIndex > aThat.sheetIndex) return -1; michael@0: if (aThat.sheetIndex > this.sheetIndex) return 1; michael@0: michael@0: if (this.ruleLine > aThat.ruleLine) return -1; michael@0: if (aThat.ruleLine > this.ruleLine) return 1; michael@0: michael@0: return 0; michael@0: }, michael@0: michael@0: toString: function CssSelectorInfo_toString() michael@0: { michael@0: return this.selector + " -> " + this.value; michael@0: }, michael@0: }; michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "domUtils", function() { michael@0: return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); michael@0: });