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: * - { display: "what to display for the given (parent) element",
michael@0: *
- element: referenceToTheElement }
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: });