|
1 /* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ts=2 et sw=2 tw=80: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 /* |
|
8 * About the objects defined in this file: |
|
9 * - CssLogic contains style information about a view context. It provides |
|
10 * access to 2 sets of objects: Css[Sheet|Rule|Selector] provide access to |
|
11 * information that does not change when the selected element changes while |
|
12 * Css[Property|Selector]Info provide information that is dependent on the |
|
13 * selected element. |
|
14 * Its key methods are highlight(), getPropertyInfo() and forEachSheet(), etc |
|
15 * It also contains a number of static methods for l10n, naming, etc |
|
16 * |
|
17 * - CssSheet provides a more useful API to a DOM CSSSheet for our purposes, |
|
18 * including shortSource and href. |
|
19 * - CssRule a more useful API to a nsIDOMCSSRule including access to the group |
|
20 * of CssSelectors that the rule provides properties for |
|
21 * - CssSelector A single selector - i.e. not a selector group. In other words |
|
22 * a CssSelector does not contain ','. This terminology is different from the |
|
23 * standard DOM API, but more inline with the definition in the spec. |
|
24 * |
|
25 * - CssPropertyInfo contains style information for a single property for the |
|
26 * highlighted element. |
|
27 * - CssSelectorInfo is a wrapper around CssSelector, which adds sorting with |
|
28 * reference to the selected element. |
|
29 */ |
|
30 |
|
31 /** |
|
32 * Provide access to the style information in a page. |
|
33 * CssLogic uses the standard DOM API, and the Gecko inIDOMUtils API to access |
|
34 * styling information in the page, and present this to the user in a way that |
|
35 * helps them understand: |
|
36 * - why their expectations may not have been fulfilled |
|
37 * - how browsers process CSS |
|
38 * @constructor |
|
39 */ |
|
40 |
|
41 const {Cc, Ci, Cu} = require("chrome"); |
|
42 |
|
43 const RX_UNIVERSAL_SELECTOR = /\s*\*\s*/g; |
|
44 const RX_NOT = /:not\((.*?)\)/g; |
|
45 const RX_PSEUDO_CLASS_OR_ELT = /(:[\w-]+\().*?\)/g; |
|
46 const RX_CONNECTORS = /\s*[\s>+~]\s*/g; |
|
47 const RX_ID = /\s*#\w+\s*/g; |
|
48 const RX_CLASS_OR_ATTRIBUTE = /\s*(?:\.\w+|\[.+?\])\s*/g; |
|
49 const RX_PSEUDO = /\s*:?:([\w-]+)(\(?\)?)\s*/g; |
|
50 |
|
51 Cu.import("resource://gre/modules/Services.jsm"); |
|
52 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
53 |
|
54 function CssLogic() |
|
55 { |
|
56 // The cache of examined CSS properties. |
|
57 _propertyInfos: {}; |
|
58 } |
|
59 |
|
60 exports.CssLogic = CssLogic; |
|
61 |
|
62 /** |
|
63 * Special values for filter, in addition to an href these values can be used |
|
64 */ |
|
65 CssLogic.FILTER = { |
|
66 USER: "user", // show properties for all user style sheets. |
|
67 UA: "ua", // USER, plus user-agent (i.e. browser) style sheets |
|
68 }; |
|
69 |
|
70 /** |
|
71 * Known media values. To distinguish "all" stylesheets (above) from "all" media |
|
72 * The full list includes braille, embossed, handheld, print, projection, |
|
73 * speech, tty, and tv, but this is only a hack because these are not defined |
|
74 * in the DOM at all. |
|
75 * @see http://www.w3.org/TR/CSS21/media.html#media-types |
|
76 */ |
|
77 CssLogic.MEDIA = { |
|
78 ALL: "all", |
|
79 SCREEN: "screen", |
|
80 }; |
|
81 |
|
82 /** |
|
83 * Each rule has a status, the bigger the number, the better placed it is to |
|
84 * provide styling information. |
|
85 * |
|
86 * These statuses are localized inside the styleinspector.properties string bundle. |
|
87 * @see csshtmltree.js RuleView._cacheStatusNames() |
|
88 */ |
|
89 CssLogic.STATUS = { |
|
90 BEST: 3, |
|
91 MATCHED: 2, |
|
92 PARENT_MATCH: 1, |
|
93 UNMATCHED: 0, |
|
94 UNKNOWN: -1, |
|
95 }; |
|
96 |
|
97 CssLogic.prototype = { |
|
98 // Both setup by highlight(). |
|
99 viewedElement: null, |
|
100 viewedDocument: null, |
|
101 |
|
102 // The cache of the known sheets. |
|
103 _sheets: null, |
|
104 |
|
105 // Have the sheets been cached? |
|
106 _sheetsCached: false, |
|
107 |
|
108 // The total number of rules, in all stylesheets, after filtering. |
|
109 _ruleCount: 0, |
|
110 |
|
111 // The computed styles for the viewedElement. |
|
112 _computedStyle: null, |
|
113 |
|
114 // Source filter. Only display properties coming from the given source |
|
115 _sourceFilter: CssLogic.FILTER.USER, |
|
116 |
|
117 // Used for tracking unique CssSheet/CssRule/CssSelector objects, in a run of |
|
118 // processMatchedSelectors(). |
|
119 _passId: 0, |
|
120 |
|
121 // Used for tracking matched CssSelector objects. |
|
122 _matchId: 0, |
|
123 |
|
124 _matchedRules: null, |
|
125 _matchedSelectors: null, |
|
126 |
|
127 /** |
|
128 * Reset various properties |
|
129 */ |
|
130 reset: function CssLogic_reset() |
|
131 { |
|
132 this._propertyInfos = {}; |
|
133 this._ruleCount = 0; |
|
134 this._sheetIndex = 0; |
|
135 this._sheets = {}; |
|
136 this._sheetsCached = false; |
|
137 this._matchedRules = null; |
|
138 this._matchedSelectors = null; |
|
139 }, |
|
140 |
|
141 /** |
|
142 * Focus on a new element - remove the style caches. |
|
143 * |
|
144 * @param {nsIDOMElement} aViewedElement the element the user has highlighted |
|
145 * in the Inspector. |
|
146 */ |
|
147 highlight: function CssLogic_highlight(aViewedElement) |
|
148 { |
|
149 if (!aViewedElement) { |
|
150 this.viewedElement = null; |
|
151 this.viewedDocument = null; |
|
152 this._computedStyle = null; |
|
153 this.reset(); |
|
154 return; |
|
155 } |
|
156 |
|
157 this.viewedElement = aViewedElement; |
|
158 |
|
159 let doc = this.viewedElement.ownerDocument; |
|
160 if (doc != this.viewedDocument) { |
|
161 // New document: clear/rebuild the cache. |
|
162 this.viewedDocument = doc; |
|
163 |
|
164 // Hunt down top level stylesheets, and cache them. |
|
165 this._cacheSheets(); |
|
166 } else { |
|
167 // Clear cached data in the CssPropertyInfo objects. |
|
168 this._propertyInfos = {}; |
|
169 } |
|
170 |
|
171 this._matchedRules = null; |
|
172 this._matchedSelectors = null; |
|
173 let win = this.viewedDocument.defaultView; |
|
174 this._computedStyle = win.getComputedStyle(this.viewedElement, ""); |
|
175 }, |
|
176 |
|
177 /** |
|
178 * Get the source filter. |
|
179 * @returns {string} The source filter being used. |
|
180 */ |
|
181 get sourceFilter() { |
|
182 return this._sourceFilter; |
|
183 }, |
|
184 |
|
185 /** |
|
186 * Source filter. Only display properties coming from the given source (web |
|
187 * address). Note that in order to avoid information overload we DO NOT show |
|
188 * unmatched system rules. |
|
189 * @see CssLogic.FILTER.* |
|
190 */ |
|
191 set sourceFilter(aValue) { |
|
192 let oldValue = this._sourceFilter; |
|
193 this._sourceFilter = aValue; |
|
194 |
|
195 let ruleCount = 0; |
|
196 |
|
197 // Update the CssSheet objects. |
|
198 this.forEachSheet(function(aSheet) { |
|
199 aSheet._sheetAllowed = -1; |
|
200 if (aSheet.contentSheet && aSheet.sheetAllowed) { |
|
201 ruleCount += aSheet.ruleCount; |
|
202 } |
|
203 }, this); |
|
204 |
|
205 this._ruleCount = ruleCount; |
|
206 |
|
207 // Full update is needed because the this.processMatchedSelectors() method |
|
208 // skips UA stylesheets if the filter does not allow such sheets. |
|
209 let needFullUpdate = (oldValue == CssLogic.FILTER.UA || |
|
210 aValue == CssLogic.FILTER.UA); |
|
211 |
|
212 if (needFullUpdate) { |
|
213 this._matchedRules = null; |
|
214 this._matchedSelectors = null; |
|
215 this._propertyInfos = {}; |
|
216 } else { |
|
217 // Update the CssPropertyInfo objects. |
|
218 for each (let propertyInfo in this._propertyInfos) { |
|
219 propertyInfo.needRefilter = true; |
|
220 } |
|
221 } |
|
222 }, |
|
223 |
|
224 /** |
|
225 * Return a CssPropertyInfo data structure for the currently viewed element |
|
226 * and the specified CSS property. If there is no currently viewed element we |
|
227 * return an empty object. |
|
228 * |
|
229 * @param {string} aProperty The CSS property to look for. |
|
230 * @return {CssPropertyInfo} a CssPropertyInfo structure for the given |
|
231 * property. |
|
232 */ |
|
233 getPropertyInfo: function CssLogic_getPropertyInfo(aProperty) |
|
234 { |
|
235 if (!this.viewedElement) { |
|
236 return {}; |
|
237 } |
|
238 |
|
239 let info = this._propertyInfos[aProperty]; |
|
240 if (!info) { |
|
241 info = new CssPropertyInfo(this, aProperty); |
|
242 this._propertyInfos[aProperty] = info; |
|
243 } |
|
244 |
|
245 return info; |
|
246 }, |
|
247 |
|
248 /** |
|
249 * Cache all the stylesheets in the inspected document |
|
250 * @private |
|
251 */ |
|
252 _cacheSheets: function CssLogic_cacheSheets() |
|
253 { |
|
254 this._passId++; |
|
255 this.reset(); |
|
256 |
|
257 // styleSheets isn't an array, but forEach can work on it anyway |
|
258 Array.prototype.forEach.call(this.viewedDocument.styleSheets, |
|
259 this._cacheSheet, this); |
|
260 |
|
261 this._sheetsCached = true; |
|
262 }, |
|
263 |
|
264 /** |
|
265 * Cache a stylesheet if it falls within the requirements: if it's enabled, |
|
266 * and if the @media is allowed. This method also walks through the stylesheet |
|
267 * cssRules to find @imported rules, to cache the stylesheets of those rules |
|
268 * as well. |
|
269 * |
|
270 * @private |
|
271 * @param {CSSStyleSheet} aDomSheet the CSSStyleSheet object to cache. |
|
272 */ |
|
273 _cacheSheet: function CssLogic_cacheSheet(aDomSheet) |
|
274 { |
|
275 if (aDomSheet.disabled) { |
|
276 return; |
|
277 } |
|
278 |
|
279 // Only work with stylesheets that have their media allowed. |
|
280 if (!this.mediaMatches(aDomSheet)) { |
|
281 return; |
|
282 } |
|
283 |
|
284 // Cache the sheet. |
|
285 let cssSheet = this.getSheet(aDomSheet, this._sheetIndex++); |
|
286 if (cssSheet._passId != this._passId) { |
|
287 cssSheet._passId = this._passId; |
|
288 |
|
289 // Find import rules. |
|
290 Array.prototype.forEach.call(aDomSheet.cssRules, function(aDomRule) { |
|
291 if (aDomRule.type == Ci.nsIDOMCSSRule.IMPORT_RULE && aDomRule.styleSheet && |
|
292 this.mediaMatches(aDomRule)) { |
|
293 this._cacheSheet(aDomRule.styleSheet); |
|
294 } |
|
295 }, this); |
|
296 } |
|
297 }, |
|
298 |
|
299 /** |
|
300 * Retrieve the list of stylesheets in the document. |
|
301 * |
|
302 * @return {array} the list of stylesheets in the document. |
|
303 */ |
|
304 get sheets() |
|
305 { |
|
306 if (!this._sheetsCached) { |
|
307 this._cacheSheets(); |
|
308 } |
|
309 |
|
310 let sheets = []; |
|
311 this.forEachSheet(function (aSheet) { |
|
312 if (aSheet.contentSheet) { |
|
313 sheets.push(aSheet); |
|
314 } |
|
315 }, this); |
|
316 |
|
317 return sheets; |
|
318 }, |
|
319 |
|
320 /** |
|
321 * Retrieve a CssSheet object for a given a CSSStyleSheet object. If the |
|
322 * stylesheet is already cached, you get the existing CssSheet object, |
|
323 * otherwise the new CSSStyleSheet object is cached. |
|
324 * |
|
325 * @param {CSSStyleSheet} aDomSheet the CSSStyleSheet object you want. |
|
326 * @param {number} aIndex the index, within the document, of the stylesheet. |
|
327 * |
|
328 * @return {CssSheet} the CssSheet object for the given CSSStyleSheet object. |
|
329 */ |
|
330 getSheet: function CL_getSheet(aDomSheet, aIndex) |
|
331 { |
|
332 let cacheId = ""; |
|
333 |
|
334 if (aDomSheet.href) { |
|
335 cacheId = aDomSheet.href; |
|
336 } else if (aDomSheet.ownerNode && aDomSheet.ownerNode.ownerDocument) { |
|
337 cacheId = aDomSheet.ownerNode.ownerDocument.location; |
|
338 } |
|
339 |
|
340 let sheet = null; |
|
341 let sheetFound = false; |
|
342 |
|
343 if (cacheId in this._sheets) { |
|
344 for (let i = 0, numSheets = this._sheets[cacheId].length; i < numSheets; i++) { |
|
345 sheet = this._sheets[cacheId][i]; |
|
346 if (sheet.domSheet === aDomSheet) { |
|
347 if (aIndex != -1) { |
|
348 sheet.index = aIndex; |
|
349 } |
|
350 sheetFound = true; |
|
351 break; |
|
352 } |
|
353 } |
|
354 } |
|
355 |
|
356 if (!sheetFound) { |
|
357 if (!(cacheId in this._sheets)) { |
|
358 this._sheets[cacheId] = []; |
|
359 } |
|
360 |
|
361 sheet = new CssSheet(this, aDomSheet, aIndex); |
|
362 if (sheet.sheetAllowed && sheet.contentSheet) { |
|
363 this._ruleCount += sheet.ruleCount; |
|
364 } |
|
365 |
|
366 this._sheets[cacheId].push(sheet); |
|
367 } |
|
368 |
|
369 return sheet; |
|
370 }, |
|
371 |
|
372 /** |
|
373 * Process each cached stylesheet in the document using your callback. |
|
374 * |
|
375 * @param {function} aCallback the function you want executed for each of the |
|
376 * CssSheet objects cached. |
|
377 * @param {object} aScope the scope you want for the callback function. aScope |
|
378 * will be the this object when aCallback executes. |
|
379 */ |
|
380 forEachSheet: function CssLogic_forEachSheet(aCallback, aScope) |
|
381 { |
|
382 for each (let sheets in this._sheets) { |
|
383 for (let i = 0; i < sheets.length; i ++) { |
|
384 // We take this as an opportunity to clean dead sheets |
|
385 try { |
|
386 let sheet = sheets[i]; |
|
387 sheet.domSheet; // If accessing domSheet raises an exception, then the |
|
388 // style sheet is a dead object |
|
389 aCallback.call(aScope, sheet, i, sheets); |
|
390 } catch (e) { |
|
391 sheets.splice(i, 1); |
|
392 i --; |
|
393 } |
|
394 } |
|
395 } |
|
396 }, |
|
397 |
|
398 /** |
|
399 * Process *some* cached stylesheets in the document using your callback. The |
|
400 * callback function should return true in order to halt processing. |
|
401 * |
|
402 * @param {function} aCallback the function you want executed for some of the |
|
403 * CssSheet objects cached. |
|
404 * @param {object} aScope the scope you want for the callback function. aScope |
|
405 * will be the this object when aCallback executes. |
|
406 * @return {Boolean} true if aCallback returns true during any iteration, |
|
407 * otherwise false is returned. |
|
408 */ |
|
409 forSomeSheets: function CssLogic_forSomeSheets(aCallback, aScope) |
|
410 { |
|
411 for each (let sheets in this._sheets) { |
|
412 if (sheets.some(aCallback, aScope)) { |
|
413 return true; |
|
414 } |
|
415 } |
|
416 return false; |
|
417 }, |
|
418 |
|
419 /** |
|
420 * Get the number nsIDOMCSSRule objects in the document, counted from all of |
|
421 * the stylesheets. System sheets are excluded. If a filter is active, this |
|
422 * tells only the number of nsIDOMCSSRule objects inside the selected |
|
423 * CSSStyleSheet. |
|
424 * |
|
425 * WARNING: This only provides an estimate of the rule count, and the results |
|
426 * could change at a later date. Todo remove this |
|
427 * |
|
428 * @return {number} the number of nsIDOMCSSRule (all rules). |
|
429 */ |
|
430 get ruleCount() |
|
431 { |
|
432 if (!this._sheetsCached) { |
|
433 this._cacheSheets(); |
|
434 } |
|
435 |
|
436 return this._ruleCount; |
|
437 }, |
|
438 |
|
439 /** |
|
440 * Process the CssSelector objects that match the highlighted element and its |
|
441 * parent elements. aScope.aCallback() is executed for each CssSelector |
|
442 * object, being passed the CssSelector object and the match status. |
|
443 * |
|
444 * This method also includes all of the element.style properties, for each |
|
445 * highlighted element parent and for the highlighted element itself. |
|
446 * |
|
447 * Note that the matched selectors are cached, such that next time your |
|
448 * callback is invoked for the cached list of CssSelector objects. |
|
449 * |
|
450 * @param {function} aCallback the function you want to execute for each of |
|
451 * the matched selectors. |
|
452 * @param {object} aScope the scope you want for the callback function. aScope |
|
453 * will be the this object when aCallback executes. |
|
454 */ |
|
455 processMatchedSelectors: function CL_processMatchedSelectors(aCallback, aScope) |
|
456 { |
|
457 if (this._matchedSelectors) { |
|
458 if (aCallback) { |
|
459 this._passId++; |
|
460 this._matchedSelectors.forEach(function(aValue) { |
|
461 aCallback.call(aScope, aValue[0], aValue[1]); |
|
462 aValue[0].cssRule._passId = this._passId; |
|
463 }, this); |
|
464 } |
|
465 return; |
|
466 } |
|
467 |
|
468 if (!this._matchedRules) { |
|
469 this._buildMatchedRules(); |
|
470 } |
|
471 |
|
472 this._matchedSelectors = []; |
|
473 this._passId++; |
|
474 |
|
475 for (let i = 0; i < this._matchedRules.length; i++) { |
|
476 let rule = this._matchedRules[i][0]; |
|
477 let status = this._matchedRules[i][1]; |
|
478 |
|
479 rule.selectors.forEach(function (aSelector) { |
|
480 if (aSelector._matchId !== this._matchId && |
|
481 (aSelector.elementStyle || |
|
482 this.selectorMatchesElement(rule.domRule, aSelector.selectorIndex))) { |
|
483 |
|
484 aSelector._matchId = this._matchId; |
|
485 this._matchedSelectors.push([ aSelector, status ]); |
|
486 if (aCallback) { |
|
487 aCallback.call(aScope, aSelector, status); |
|
488 } |
|
489 } |
|
490 }, this); |
|
491 |
|
492 rule._passId = this._passId; |
|
493 } |
|
494 }, |
|
495 |
|
496 /** |
|
497 * Check if the given selector matches the highlighted element or any of its |
|
498 * parents. |
|
499 * |
|
500 * @private |
|
501 * @param {DOMRule} domRule |
|
502 * The DOM Rule containing the selector. |
|
503 * @param {Number} idx |
|
504 * The index of the selector within the DOMRule. |
|
505 * @return {boolean} |
|
506 * true if the given selector matches the highlighted element or any |
|
507 * of its parents, otherwise false is returned. |
|
508 */ |
|
509 selectorMatchesElement: function CL_selectorMatchesElement2(domRule, idx) |
|
510 { |
|
511 let element = this.viewedElement; |
|
512 do { |
|
513 if (domUtils.selectorMatchesElement(element, domRule, idx)) { |
|
514 return true; |
|
515 } |
|
516 } while ((element = element.parentNode) && |
|
517 element.nodeType === Ci.nsIDOMNode.ELEMENT_NODE); |
|
518 |
|
519 return false; |
|
520 }, |
|
521 |
|
522 /** |
|
523 * Check if the highlighted element or it's parents have matched selectors. |
|
524 * |
|
525 * @param {array} aProperties The list of properties you want to check if they |
|
526 * have matched selectors or not. |
|
527 * @return {object} An object that tells for each property if it has matched |
|
528 * selectors or not. Object keys are property names and values are booleans. |
|
529 */ |
|
530 hasMatchedSelectors: function CL_hasMatchedSelectors(aProperties) |
|
531 { |
|
532 if (!this._matchedRules) { |
|
533 this._buildMatchedRules(); |
|
534 } |
|
535 |
|
536 let result = {}; |
|
537 |
|
538 this._matchedRules.some(function(aValue) { |
|
539 let rule = aValue[0]; |
|
540 let status = aValue[1]; |
|
541 aProperties = aProperties.filter(function(aProperty) { |
|
542 // We just need to find if a rule has this property while it matches |
|
543 // the viewedElement (or its parents). |
|
544 if (rule.getPropertyValue(aProperty) && |
|
545 (status == CssLogic.STATUS.MATCHED || |
|
546 (status == CssLogic.STATUS.PARENT_MATCH && |
|
547 domUtils.isInheritedProperty(aProperty)))) { |
|
548 result[aProperty] = true; |
|
549 return false; |
|
550 } |
|
551 return true; // Keep the property for the next rule. |
|
552 }.bind(this)); |
|
553 return aProperties.length == 0; |
|
554 }, this); |
|
555 |
|
556 return result; |
|
557 }, |
|
558 |
|
559 /** |
|
560 * Build the array of matched rules for the currently highlighted element. |
|
561 * The array will hold rules that match the viewedElement and its parents. |
|
562 * |
|
563 * @private |
|
564 */ |
|
565 _buildMatchedRules: function CL__buildMatchedRules() |
|
566 { |
|
567 let domRules; |
|
568 let element = this.viewedElement; |
|
569 let filter = this.sourceFilter; |
|
570 let sheetIndex = 0; |
|
571 |
|
572 this._matchId++; |
|
573 this._passId++; |
|
574 this._matchedRules = []; |
|
575 |
|
576 if (!element) { |
|
577 return; |
|
578 } |
|
579 |
|
580 do { |
|
581 let status = this.viewedElement === element ? |
|
582 CssLogic.STATUS.MATCHED : CssLogic.STATUS.PARENT_MATCH; |
|
583 |
|
584 try { |
|
585 domRules = domUtils.getCSSStyleRules(element); |
|
586 } catch (ex) { |
|
587 Services.console. |
|
588 logStringMessage("CL__buildMatchedRules error: " + ex); |
|
589 continue; |
|
590 } |
|
591 |
|
592 for (let i = 0, n = domRules.Count(); i < n; i++) { |
|
593 let domRule = domRules.GetElementAt(i); |
|
594 if (domRule.type !== Ci.nsIDOMCSSRule.STYLE_RULE) { |
|
595 continue; |
|
596 } |
|
597 |
|
598 let sheet = this.getSheet(domRule.parentStyleSheet, -1); |
|
599 if (sheet._passId !== this._passId) { |
|
600 sheet.index = sheetIndex++; |
|
601 sheet._passId = this._passId; |
|
602 } |
|
603 |
|
604 if (filter === CssLogic.FILTER.USER && !sheet.contentSheet) { |
|
605 continue; |
|
606 } |
|
607 |
|
608 let rule = sheet.getRule(domRule); |
|
609 if (rule._passId === this._passId) { |
|
610 continue; |
|
611 } |
|
612 |
|
613 rule._matchId = this._matchId; |
|
614 rule._passId = this._passId; |
|
615 this._matchedRules.push([rule, status]); |
|
616 } |
|
617 |
|
618 |
|
619 // Add element.style information. |
|
620 if (element.style && element.style.length > 0) { |
|
621 let rule = new CssRule(null, { style: element.style }, element); |
|
622 rule._matchId = this._matchId; |
|
623 rule._passId = this._passId; |
|
624 this._matchedRules.push([rule, status]); |
|
625 } |
|
626 } while ((element = element.parentNode) && |
|
627 element.nodeType === Ci.nsIDOMNode.ELEMENT_NODE); |
|
628 }, |
|
629 |
|
630 /** |
|
631 * Tells if the given DOM CSS object matches the current view media. |
|
632 * |
|
633 * @param {object} aDomObject The DOM CSS object to check. |
|
634 * @return {boolean} True if the DOM CSS object matches the current view |
|
635 * media, or false otherwise. |
|
636 */ |
|
637 mediaMatches: function CL_mediaMatches(aDomObject) |
|
638 { |
|
639 let mediaText = aDomObject.media.mediaText; |
|
640 return !mediaText || this.viewedDocument.defaultView. |
|
641 matchMedia(mediaText).matches; |
|
642 }, |
|
643 }; |
|
644 |
|
645 /** |
|
646 * If the element has an id, return '#id'. Otherwise return 'tagname[n]' where |
|
647 * n is the index of this element in its siblings. |
|
648 * <p>A technically more 'correct' output from the no-id case might be: |
|
649 * 'tagname:nth-of-type(n)' however this is unlikely to be more understood |
|
650 * and it is longer. |
|
651 * |
|
652 * @param {nsIDOMElement} aElement the element for which you want the short name. |
|
653 * @return {string} the string to be displayed for aElement. |
|
654 */ |
|
655 CssLogic.getShortName = function CssLogic_getShortName(aElement) |
|
656 { |
|
657 if (!aElement) { |
|
658 return "null"; |
|
659 } |
|
660 if (aElement.id) { |
|
661 return "#" + aElement.id; |
|
662 } |
|
663 let priorSiblings = 0; |
|
664 let temp = aElement; |
|
665 while (temp = temp.previousElementSibling) { |
|
666 priorSiblings++; |
|
667 } |
|
668 return aElement.tagName + "[" + priorSiblings + "]"; |
|
669 }; |
|
670 |
|
671 /** |
|
672 * Get an array of short names from the given element to document.body. |
|
673 * |
|
674 * @param {nsIDOMElement} aElement the element for which you want the array of |
|
675 * short names. |
|
676 * @return {array} The array of elements. |
|
677 * <p>Each element is an object of the form: |
|
678 * <ul> |
|
679 * <li>{ display: "what to display for the given (parent) element", |
|
680 * <li> element: referenceToTheElement } |
|
681 * </ul> |
|
682 */ |
|
683 CssLogic.getShortNamePath = function CssLogic_getShortNamePath(aElement) |
|
684 { |
|
685 let doc = aElement.ownerDocument; |
|
686 let reply = []; |
|
687 |
|
688 if (!aElement) { |
|
689 return reply; |
|
690 } |
|
691 |
|
692 // We want to exclude nodes high up the tree (body/html) unless the user |
|
693 // has selected that node, in which case we need to report something. |
|
694 do { |
|
695 reply.unshift({ |
|
696 display: CssLogic.getShortName(aElement), |
|
697 element: aElement |
|
698 }); |
|
699 aElement = aElement.parentNode; |
|
700 } while (aElement && aElement != doc.body && aElement != doc.head && aElement != doc); |
|
701 |
|
702 return reply; |
|
703 }; |
|
704 |
|
705 /** |
|
706 * Get a string list of selectors for a given DOMRule. |
|
707 * |
|
708 * @param {DOMRule} aDOMRule |
|
709 * The DOMRule to parse. |
|
710 * @return {Array} |
|
711 * An array of string selectors. |
|
712 */ |
|
713 CssLogic.getSelectors = function CssLogic_getSelectors(aDOMRule) |
|
714 { |
|
715 let selectors = []; |
|
716 |
|
717 let len = domUtils.getSelectorCount(aDOMRule); |
|
718 for (let i = 0; i < len; i++) { |
|
719 let text = domUtils.getSelectorText(aDOMRule, i); |
|
720 selectors.push(text); |
|
721 } |
|
722 return selectors; |
|
723 } |
|
724 |
|
725 /** |
|
726 * Memonized lookup of a l10n string from a string bundle. |
|
727 * @param {string} aName The key to lookup. |
|
728 * @returns A localized version of the given key. |
|
729 */ |
|
730 CssLogic.l10n = function(aName) CssLogic._strings.GetStringFromName(aName); |
|
731 |
|
732 XPCOMUtils.defineLazyGetter(CssLogic, "_strings", function() Services.strings |
|
733 .createBundle("chrome://global/locale/devtools/styleinspector.properties")); |
|
734 |
|
735 /** |
|
736 * Is the given property sheet a content stylesheet? |
|
737 * |
|
738 * @param {CSSStyleSheet} aSheet a stylesheet |
|
739 * @return {boolean} true if the given stylesheet is a content stylesheet, |
|
740 * false otherwise. |
|
741 */ |
|
742 CssLogic.isContentStylesheet = function CssLogic_isContentStylesheet(aSheet) |
|
743 { |
|
744 // All sheets with owner nodes have been included by content. |
|
745 if (aSheet.ownerNode) { |
|
746 return true; |
|
747 } |
|
748 |
|
749 // If the sheet has a CSSImportRule we need to check the parent stylesheet. |
|
750 if (aSheet.ownerRule instanceof Ci.nsIDOMCSSImportRule) { |
|
751 return CssLogic.isContentStylesheet(aSheet.parentStyleSheet); |
|
752 } |
|
753 |
|
754 return false; |
|
755 }; |
|
756 |
|
757 /** |
|
758 * Get a source for a stylesheet, taking into account embedded stylesheets |
|
759 * for which we need to use document.defaultView.location.href rather than |
|
760 * sheet.href |
|
761 * |
|
762 * @param {CSSStyleSheet} aSheet the DOM object for the style sheet. |
|
763 * @return {string} the address of the stylesheet. |
|
764 */ |
|
765 CssLogic.href = function CssLogic_href(aSheet) |
|
766 { |
|
767 let href = aSheet.href; |
|
768 if (!href) { |
|
769 href = aSheet.ownerNode.ownerDocument.location; |
|
770 } |
|
771 |
|
772 return href; |
|
773 }; |
|
774 |
|
775 /** |
|
776 * Return a shortened version of a style sheet's source. |
|
777 * |
|
778 * @param {CSSStyleSheet} aSheet the DOM object for the style sheet. |
|
779 */ |
|
780 CssLogic.shortSource = function CssLogic_shortSource(aSheet) |
|
781 { |
|
782 // Use a string like "inline" if there is no source href |
|
783 if (!aSheet || !aSheet.href) { |
|
784 return CssLogic.l10n("rule.sourceInline"); |
|
785 } |
|
786 |
|
787 // We try, in turn, the filename, filePath, query string, whole thing |
|
788 let url = {}; |
|
789 try { |
|
790 url = Services.io.newURI(aSheet.href, null, null); |
|
791 url = url.QueryInterface(Ci.nsIURL); |
|
792 } catch (ex) { |
|
793 // Some UA-provided stylesheets are not valid URLs. |
|
794 } |
|
795 |
|
796 if (url.fileName) { |
|
797 return url.fileName; |
|
798 } |
|
799 |
|
800 if (url.filePath) { |
|
801 return url.filePath; |
|
802 } |
|
803 |
|
804 if (url.query) { |
|
805 return url.query; |
|
806 } |
|
807 |
|
808 let dataUrl = aSheet.href.match(/^(data:[^,]*),/); |
|
809 return dataUrl ? dataUrl[1] : aSheet.href; |
|
810 } |
|
811 |
|
812 /** |
|
813 * Extract the background image URL (if any) from a property value. |
|
814 * Used, for example, for the preview tooltip in the rule view and |
|
815 * computed view. |
|
816 * |
|
817 * @param {String} aProperty |
|
818 * @param {String} aSheetHref |
|
819 * @return {string} a image URL |
|
820 */ |
|
821 CssLogic.getBackgroundImageUriFromProperty = function(aProperty, aSheetHref) { |
|
822 let startToken = "url(", start = aProperty.indexOf(startToken), end; |
|
823 if (start === -1) { |
|
824 return null; |
|
825 } |
|
826 |
|
827 aProperty = aProperty.substring(start + startToken.length).trim(); |
|
828 let quote = aProperty.substring(0, 1); |
|
829 if (quote === "'" || quote === '"') { |
|
830 end = aProperty.search(new RegExp(quote + "\\s*\\)")); |
|
831 start = 1; |
|
832 } else { |
|
833 end = aProperty.indexOf(")"); |
|
834 start = 0; |
|
835 } |
|
836 |
|
837 let uri = aProperty.substring(start, end).trim(); |
|
838 if (aSheetHref) { |
|
839 let IOService = Cc["@mozilla.org/network/io-service;1"] |
|
840 .getService(Ci.nsIIOService); |
|
841 let sheetUri = IOService.newURI(aSheetHref, null, null); |
|
842 uri = sheetUri.resolve(uri); |
|
843 } |
|
844 |
|
845 return uri; |
|
846 } |
|
847 |
|
848 /** |
|
849 * Find the position of [element] in [nodeList]. |
|
850 * @returns an index of the match, or -1 if there is no match |
|
851 */ |
|
852 function positionInNodeList(element, nodeList) { |
|
853 for (var i = 0; i < nodeList.length; i++) { |
|
854 if (element === nodeList[i]) { |
|
855 return i; |
|
856 } |
|
857 } |
|
858 return -1; |
|
859 } |
|
860 |
|
861 /** |
|
862 * Find a unique CSS selector for a given element |
|
863 * @returns a string such that ele.ownerDocument.querySelector(reply) === ele |
|
864 * and ele.ownerDocument.querySelectorAll(reply).length === 1 |
|
865 */ |
|
866 CssLogic.findCssSelector = function CssLogic_findCssSelector(ele) { |
|
867 var document = ele.ownerDocument; |
|
868 if (ele.id && document.getElementById(ele.id) === ele) { |
|
869 return '#' + ele.id; |
|
870 } |
|
871 |
|
872 // Inherently unique by tag name |
|
873 var tagName = ele.tagName.toLowerCase(); |
|
874 if (tagName === 'html') { |
|
875 return 'html'; |
|
876 } |
|
877 if (tagName === 'head') { |
|
878 return 'head'; |
|
879 } |
|
880 if (tagName === 'body') { |
|
881 return 'body'; |
|
882 } |
|
883 |
|
884 if (ele.parentNode == null) { |
|
885 console.log('danger: ' + tagName); |
|
886 } |
|
887 |
|
888 // We might be able to find a unique class name |
|
889 var selector, index, matches; |
|
890 if (ele.classList.length > 0) { |
|
891 for (var i = 0; i < ele.classList.length; i++) { |
|
892 // Is this className unique by itself? |
|
893 selector = '.' + ele.classList.item(i); |
|
894 matches = document.querySelectorAll(selector); |
|
895 if (matches.length === 1) { |
|
896 return selector; |
|
897 } |
|
898 // Maybe it's unique with a tag name? |
|
899 selector = tagName + selector; |
|
900 matches = document.querySelectorAll(selector); |
|
901 if (matches.length === 1) { |
|
902 return selector; |
|
903 } |
|
904 // Maybe it's unique using a tag name and nth-child |
|
905 index = positionInNodeList(ele, ele.parentNode.children) + 1; |
|
906 selector = selector + ':nth-child(' + index + ')'; |
|
907 matches = document.querySelectorAll(selector); |
|
908 if (matches.length === 1) { |
|
909 return selector; |
|
910 } |
|
911 } |
|
912 } |
|
913 |
|
914 // So we can be unique w.r.t. our parent, and use recursion |
|
915 index = positionInNodeList(ele, ele.parentNode.children) + 1; |
|
916 selector = CssLogic_findCssSelector(ele.parentNode) + ' > ' + |
|
917 tagName + ':nth-child(' + index + ')'; |
|
918 |
|
919 return selector; |
|
920 }; |
|
921 |
|
922 /** |
|
923 * A safe way to access cached bits of information about a stylesheet. |
|
924 * |
|
925 * @constructor |
|
926 * @param {CssLogic} aCssLogic pointer to the CssLogic instance working with |
|
927 * this CssSheet object. |
|
928 * @param {CSSStyleSheet} aDomSheet reference to a DOM CSSStyleSheet object. |
|
929 * @param {number} aIndex tells the index/position of the stylesheet within the |
|
930 * main document. |
|
931 */ |
|
932 function CssSheet(aCssLogic, aDomSheet, aIndex) |
|
933 { |
|
934 this._cssLogic = aCssLogic; |
|
935 this.domSheet = aDomSheet; |
|
936 this.index = this.contentSheet ? aIndex : -100 * aIndex; |
|
937 |
|
938 // Cache of the sheets href. Cached by the getter. |
|
939 this._href = null; |
|
940 // Short version of href for use in select boxes etc. Cached by getter. |
|
941 this._shortSource = null; |
|
942 |
|
943 // null for uncached. |
|
944 this._sheetAllowed = null; |
|
945 |
|
946 // Cached CssRules from the given stylesheet. |
|
947 this._rules = {}; |
|
948 |
|
949 this._ruleCount = -1; |
|
950 } |
|
951 |
|
952 CssSheet.prototype = { |
|
953 _passId: null, |
|
954 _contentSheet: null, |
|
955 _mediaMatches: null, |
|
956 |
|
957 /** |
|
958 * Tells if the stylesheet is provided by the browser or not. |
|
959 * |
|
960 * @return {boolean} false if this is a browser-provided stylesheet, or true |
|
961 * otherwise. |
|
962 */ |
|
963 get contentSheet() |
|
964 { |
|
965 if (this._contentSheet === null) { |
|
966 this._contentSheet = CssLogic.isContentStylesheet(this.domSheet); |
|
967 } |
|
968 return this._contentSheet; |
|
969 }, |
|
970 |
|
971 /** |
|
972 * Tells if the stylesheet is disabled or not. |
|
973 * @return {boolean} true if this stylesheet is disabled, or false otherwise. |
|
974 */ |
|
975 get disabled() |
|
976 { |
|
977 return this.domSheet.disabled; |
|
978 }, |
|
979 |
|
980 /** |
|
981 * Tells if the stylesheet matches the current browser view media. |
|
982 * @return {boolean} true if this stylesheet matches the current browser view |
|
983 * media, or false otherwise. |
|
984 */ |
|
985 get mediaMatches() |
|
986 { |
|
987 if (this._mediaMatches === null) { |
|
988 this._mediaMatches = this._cssLogic.mediaMatches(this.domSheet); |
|
989 } |
|
990 return this._mediaMatches; |
|
991 }, |
|
992 |
|
993 /** |
|
994 * Get a source for a stylesheet, using CssLogic.href |
|
995 * |
|
996 * @return {string} the address of the stylesheet. |
|
997 */ |
|
998 get href() |
|
999 { |
|
1000 if (this._href) { |
|
1001 return this._href; |
|
1002 } |
|
1003 |
|
1004 this._href = CssLogic.href(this.domSheet); |
|
1005 return this._href; |
|
1006 }, |
|
1007 |
|
1008 /** |
|
1009 * Create a shorthand version of the href of a stylesheet. |
|
1010 * |
|
1011 * @return {string} the shorthand source of the stylesheet. |
|
1012 */ |
|
1013 get shortSource() |
|
1014 { |
|
1015 if (this._shortSource) { |
|
1016 return this._shortSource; |
|
1017 } |
|
1018 |
|
1019 this._shortSource = CssLogic.shortSource(this.domSheet); |
|
1020 return this._shortSource; |
|
1021 }, |
|
1022 |
|
1023 /** |
|
1024 * Tells if the sheet is allowed or not by the current CssLogic.sourceFilter. |
|
1025 * |
|
1026 * @return {boolean} true if the stylesheet is allowed by the sourceFilter, or |
|
1027 * false otherwise. |
|
1028 */ |
|
1029 get sheetAllowed() |
|
1030 { |
|
1031 if (this._sheetAllowed !== null) { |
|
1032 return this._sheetAllowed; |
|
1033 } |
|
1034 |
|
1035 this._sheetAllowed = true; |
|
1036 |
|
1037 let filter = this._cssLogic.sourceFilter; |
|
1038 if (filter === CssLogic.FILTER.USER && !this.contentSheet) { |
|
1039 this._sheetAllowed = false; |
|
1040 } |
|
1041 if (filter !== CssLogic.FILTER.USER && filter !== CssLogic.FILTER.UA) { |
|
1042 this._sheetAllowed = (filter === this.href); |
|
1043 } |
|
1044 |
|
1045 return this._sheetAllowed; |
|
1046 }, |
|
1047 |
|
1048 /** |
|
1049 * Retrieve the number of rules in this stylesheet. |
|
1050 * |
|
1051 * @return {number} the number of nsIDOMCSSRule objects in this stylesheet. |
|
1052 */ |
|
1053 get ruleCount() |
|
1054 { |
|
1055 return this._ruleCount > -1 ? |
|
1056 this._ruleCount : |
|
1057 this.domSheet.cssRules.length; |
|
1058 }, |
|
1059 |
|
1060 /** |
|
1061 * Retrieve a CssRule object for the given CSSStyleRule. The CssRule object is |
|
1062 * cached, such that subsequent retrievals return the same CssRule object for |
|
1063 * the same CSSStyleRule object. |
|
1064 * |
|
1065 * @param {CSSStyleRule} aDomRule the CSSStyleRule object for which you want a |
|
1066 * CssRule object. |
|
1067 * @return {CssRule} the cached CssRule object for the given CSSStyleRule |
|
1068 * object. |
|
1069 */ |
|
1070 getRule: function CssSheet_getRule(aDomRule) |
|
1071 { |
|
1072 let cacheId = aDomRule.type + aDomRule.selectorText; |
|
1073 |
|
1074 let rule = null; |
|
1075 let ruleFound = false; |
|
1076 |
|
1077 if (cacheId in this._rules) { |
|
1078 for (let i = 0, rulesLen = this._rules[cacheId].length; i < rulesLen; i++) { |
|
1079 rule = this._rules[cacheId][i]; |
|
1080 if (rule.domRule === aDomRule) { |
|
1081 ruleFound = true; |
|
1082 break; |
|
1083 } |
|
1084 } |
|
1085 } |
|
1086 |
|
1087 if (!ruleFound) { |
|
1088 if (!(cacheId in this._rules)) { |
|
1089 this._rules[cacheId] = []; |
|
1090 } |
|
1091 |
|
1092 rule = new CssRule(this, aDomRule); |
|
1093 this._rules[cacheId].push(rule); |
|
1094 } |
|
1095 |
|
1096 return rule; |
|
1097 }, |
|
1098 |
|
1099 /** |
|
1100 * Process each rule in this stylesheet using your callback function. Your |
|
1101 * function receives one argument: the CssRule object for each CSSStyleRule |
|
1102 * inside the stylesheet. |
|
1103 * |
|
1104 * Note that this method also iterates through @media rules inside the |
|
1105 * stylesheet. |
|
1106 * |
|
1107 * @param {function} aCallback the function you want to execute for each of |
|
1108 * the style rules. |
|
1109 * @param {object} aScope the scope you want for the callback function. aScope |
|
1110 * will be the this object when aCallback executes. |
|
1111 */ |
|
1112 forEachRule: function CssSheet_forEachRule(aCallback, aScope) |
|
1113 { |
|
1114 let ruleCount = 0; |
|
1115 let domRules = this.domSheet.cssRules; |
|
1116 |
|
1117 function _iterator(aDomRule) { |
|
1118 if (aDomRule.type == Ci.nsIDOMCSSRule.STYLE_RULE) { |
|
1119 aCallback.call(aScope, this.getRule(aDomRule)); |
|
1120 ruleCount++; |
|
1121 } else if (aDomRule.type == Ci.nsIDOMCSSRule.MEDIA_RULE && |
|
1122 aDomRule.cssRules && this._cssLogic.mediaMatches(aDomRule)) { |
|
1123 Array.prototype.forEach.call(aDomRule.cssRules, _iterator, this); |
|
1124 } |
|
1125 } |
|
1126 |
|
1127 Array.prototype.forEach.call(domRules, _iterator, this); |
|
1128 |
|
1129 this._ruleCount = ruleCount; |
|
1130 }, |
|
1131 |
|
1132 /** |
|
1133 * Process *some* rules in this stylesheet using your callback function. Your |
|
1134 * function receives one argument: the CssRule object for each CSSStyleRule |
|
1135 * inside the stylesheet. In order to stop processing the callback function |
|
1136 * needs to return a value. |
|
1137 * |
|
1138 * Note that this method also iterates through @media rules inside the |
|
1139 * stylesheet. |
|
1140 * |
|
1141 * @param {function} aCallback the function you want to execute for each of |
|
1142 * the style rules. |
|
1143 * @param {object} aScope the scope you want for the callback function. aScope |
|
1144 * will be the this object when aCallback executes. |
|
1145 * @return {Boolean} true if aCallback returns true during any iteration, |
|
1146 * otherwise false is returned. |
|
1147 */ |
|
1148 forSomeRules: function CssSheet_forSomeRules(aCallback, aScope) |
|
1149 { |
|
1150 let domRules = this.domSheet.cssRules; |
|
1151 function _iterator(aDomRule) { |
|
1152 if (aDomRule.type == Ci.nsIDOMCSSRule.STYLE_RULE) { |
|
1153 return aCallback.call(aScope, this.getRule(aDomRule)); |
|
1154 } else if (aDomRule.type == Ci.nsIDOMCSSRule.MEDIA_RULE && |
|
1155 aDomRule.cssRules && this._cssLogic.mediaMatches(aDomRule)) { |
|
1156 return Array.prototype.some.call(aDomRule.cssRules, _iterator, this); |
|
1157 } |
|
1158 } |
|
1159 return Array.prototype.some.call(domRules, _iterator, this); |
|
1160 }, |
|
1161 |
|
1162 toString: function CssSheet_toString() |
|
1163 { |
|
1164 return "CssSheet[" + this.shortSource + "]"; |
|
1165 } |
|
1166 }; |
|
1167 |
|
1168 /** |
|
1169 * Information about a single CSSStyleRule. |
|
1170 * |
|
1171 * @param {CSSSheet|null} aCssSheet the CssSheet object of the stylesheet that |
|
1172 * holds the CSSStyleRule. If the rule comes from element.style, set this |
|
1173 * argument to null. |
|
1174 * @param {CSSStyleRule|object} aDomRule the DOM CSSStyleRule for which you want |
|
1175 * to cache data. If the rule comes from element.style, then provide |
|
1176 * an object of the form: {style: element.style}. |
|
1177 * @param {Element} [aElement] If the rule comes from element.style, then this |
|
1178 * argument must point to the element. |
|
1179 * @constructor |
|
1180 */ |
|
1181 function CssRule(aCssSheet, aDomRule, aElement) |
|
1182 { |
|
1183 this._cssSheet = aCssSheet; |
|
1184 this.domRule = aDomRule; |
|
1185 |
|
1186 let parentRule = aDomRule.parentRule; |
|
1187 if (parentRule && parentRule.type == Ci.nsIDOMCSSRule.MEDIA_RULE) { |
|
1188 this.mediaText = parentRule.media.mediaText; |
|
1189 } |
|
1190 |
|
1191 if (this._cssSheet) { |
|
1192 // parse domRule.selectorText on call to this.selectors |
|
1193 this._selectors = null; |
|
1194 this.line = domUtils.getRuleLine(this.domRule); |
|
1195 this.source = this._cssSheet.shortSource + ":" + this.line; |
|
1196 if (this.mediaText) { |
|
1197 this.source += " @media " + this.mediaText; |
|
1198 } |
|
1199 this.href = this._cssSheet.href; |
|
1200 this.contentRule = this._cssSheet.contentSheet; |
|
1201 } else if (aElement) { |
|
1202 this._selectors = [ new CssSelector(this, "@element.style", 0) ]; |
|
1203 this.line = -1; |
|
1204 this.source = CssLogic.l10n("rule.sourceElement"); |
|
1205 this.href = "#"; |
|
1206 this.contentRule = true; |
|
1207 this.sourceElement = aElement; |
|
1208 } |
|
1209 } |
|
1210 |
|
1211 CssRule.prototype = { |
|
1212 _passId: null, |
|
1213 |
|
1214 mediaText: "", |
|
1215 |
|
1216 get isMediaRule() |
|
1217 { |
|
1218 return !!this.mediaText; |
|
1219 }, |
|
1220 |
|
1221 /** |
|
1222 * Check if the parent stylesheet is allowed by the CssLogic.sourceFilter. |
|
1223 * |
|
1224 * @return {boolean} true if the parent stylesheet is allowed by the current |
|
1225 * sourceFilter, or false otherwise. |
|
1226 */ |
|
1227 get sheetAllowed() |
|
1228 { |
|
1229 return this._cssSheet ? this._cssSheet.sheetAllowed : true; |
|
1230 }, |
|
1231 |
|
1232 /** |
|
1233 * Retrieve the parent stylesheet index/position in the viewed document. |
|
1234 * |
|
1235 * @return {number} the parent stylesheet index/position in the viewed |
|
1236 * document. |
|
1237 */ |
|
1238 get sheetIndex() |
|
1239 { |
|
1240 return this._cssSheet ? this._cssSheet.index : 0; |
|
1241 }, |
|
1242 |
|
1243 /** |
|
1244 * Retrieve the style property value from the current CSSStyleRule. |
|
1245 * |
|
1246 * @param {string} aProperty the CSS property name for which you want the |
|
1247 * value. |
|
1248 * @return {string} the property value. |
|
1249 */ |
|
1250 getPropertyValue: function(aProperty) |
|
1251 { |
|
1252 return this.domRule.style.getPropertyValue(aProperty); |
|
1253 }, |
|
1254 |
|
1255 /** |
|
1256 * Retrieve the style property priority from the current CSSStyleRule. |
|
1257 * |
|
1258 * @param {string} aProperty the CSS property name for which you want the |
|
1259 * priority. |
|
1260 * @return {string} the property priority. |
|
1261 */ |
|
1262 getPropertyPriority: function(aProperty) |
|
1263 { |
|
1264 return this.domRule.style.getPropertyPriority(aProperty); |
|
1265 }, |
|
1266 |
|
1267 /** |
|
1268 * Retrieve the list of CssSelector objects for each of the parsed selectors |
|
1269 * of the current CSSStyleRule. |
|
1270 * |
|
1271 * @return {array} the array hold the CssSelector objects. |
|
1272 */ |
|
1273 get selectors() |
|
1274 { |
|
1275 if (this._selectors) { |
|
1276 return this._selectors; |
|
1277 } |
|
1278 |
|
1279 // Parse the CSSStyleRule.selectorText string. |
|
1280 this._selectors = []; |
|
1281 |
|
1282 if (!this.domRule.selectorText) { |
|
1283 return this._selectors; |
|
1284 } |
|
1285 |
|
1286 let selectors = CssLogic.getSelectors(this.domRule); |
|
1287 |
|
1288 for (let i = 0, len = selectors.length; i < len; i++) { |
|
1289 this._selectors.push(new CssSelector(this, selectors[i], i)); |
|
1290 } |
|
1291 |
|
1292 return this._selectors; |
|
1293 }, |
|
1294 |
|
1295 toString: function CssRule_toString() |
|
1296 { |
|
1297 return "[CssRule " + this.domRule.selectorText + "]"; |
|
1298 }, |
|
1299 }; |
|
1300 |
|
1301 /** |
|
1302 * The CSS selector class allows us to document the ranking of various CSS |
|
1303 * selectors. |
|
1304 * |
|
1305 * @constructor |
|
1306 * @param {CssRule} aCssRule the CssRule instance from where the selector comes. |
|
1307 * @param {string} aSelector The selector that we wish to investigate. |
|
1308 * @param {Number} aIndex The index of the selector within it's rule. |
|
1309 */ |
|
1310 function CssSelector(aCssRule, aSelector, aIndex) |
|
1311 { |
|
1312 this.cssRule = aCssRule; |
|
1313 this.text = aSelector; |
|
1314 this.elementStyle = this.text == "@element.style"; |
|
1315 this._specificity = null; |
|
1316 this.selectorIndex = aIndex; |
|
1317 } |
|
1318 |
|
1319 exports.CssSelector = CssSelector; |
|
1320 |
|
1321 CssSelector.prototype = { |
|
1322 _matchId: null, |
|
1323 |
|
1324 /** |
|
1325 * Retrieve the CssSelector source, which is the source of the CssSheet owning |
|
1326 * the selector. |
|
1327 * |
|
1328 * @return {string} the selector source. |
|
1329 */ |
|
1330 get source() |
|
1331 { |
|
1332 return this.cssRule.source; |
|
1333 }, |
|
1334 |
|
1335 /** |
|
1336 * Retrieve the CssSelector source element, which is the source of the CssRule |
|
1337 * owning the selector. This is only available when the CssSelector comes from |
|
1338 * an element.style. |
|
1339 * |
|
1340 * @return {string} the source element selector. |
|
1341 */ |
|
1342 get sourceElement() |
|
1343 { |
|
1344 return this.cssRule.sourceElement; |
|
1345 }, |
|
1346 |
|
1347 /** |
|
1348 * Retrieve the address of the CssSelector. This points to the address of the |
|
1349 * CssSheet owning this selector. |
|
1350 * |
|
1351 * @return {string} the address of the CssSelector. |
|
1352 */ |
|
1353 get href() |
|
1354 { |
|
1355 return this.cssRule.href; |
|
1356 }, |
|
1357 |
|
1358 /** |
|
1359 * Check if the selector comes from a browser-provided stylesheet. |
|
1360 * |
|
1361 * @return {boolean} true if the selector comes from a content-provided |
|
1362 * stylesheet, or false otherwise. |
|
1363 */ |
|
1364 get contentRule() |
|
1365 { |
|
1366 return this.cssRule.contentRule; |
|
1367 }, |
|
1368 |
|
1369 /** |
|
1370 * Check if the parent stylesheet is allowed by the CssLogic.sourceFilter. |
|
1371 * |
|
1372 * @return {boolean} true if the parent stylesheet is allowed by the current |
|
1373 * sourceFilter, or false otherwise. |
|
1374 */ |
|
1375 get sheetAllowed() |
|
1376 { |
|
1377 return this.cssRule.sheetAllowed; |
|
1378 }, |
|
1379 |
|
1380 /** |
|
1381 * Retrieve the parent stylesheet index/position in the viewed document. |
|
1382 * |
|
1383 * @return {number} the parent stylesheet index/position in the viewed |
|
1384 * document. |
|
1385 */ |
|
1386 get sheetIndex() |
|
1387 { |
|
1388 return this.cssRule.sheetIndex; |
|
1389 }, |
|
1390 |
|
1391 /** |
|
1392 * Retrieve the line of the parent CSSStyleRule in the parent CSSStyleSheet. |
|
1393 * |
|
1394 * @return {number} the line of the parent CSSStyleRule in the parent |
|
1395 * stylesheet. |
|
1396 */ |
|
1397 get ruleLine() |
|
1398 { |
|
1399 return this.cssRule.line; |
|
1400 }, |
|
1401 |
|
1402 /** |
|
1403 * Retrieve the pseudo-elements that we support. This list should match the |
|
1404 * elements specified in layout/style/nsCSSPseudoElementList.h |
|
1405 */ |
|
1406 get pseudoElements() |
|
1407 { |
|
1408 if (!CssSelector._pseudoElements) { |
|
1409 let pseudos = CssSelector._pseudoElements = new Set(); |
|
1410 pseudos.add("after"); |
|
1411 pseudos.add("before"); |
|
1412 pseudos.add("first-letter"); |
|
1413 pseudos.add("first-line"); |
|
1414 pseudos.add("selection"); |
|
1415 pseudos.add("-moz-color-swatch"); |
|
1416 pseudos.add("-moz-focus-inner"); |
|
1417 pseudos.add("-moz-focus-outer"); |
|
1418 pseudos.add("-moz-list-bullet"); |
|
1419 pseudos.add("-moz-list-number"); |
|
1420 pseudos.add("-moz-math-anonymous"); |
|
1421 pseudos.add("-moz-math-stretchy"); |
|
1422 pseudos.add("-moz-progress-bar"); |
|
1423 pseudos.add("-moz-selection"); |
|
1424 } |
|
1425 return CssSelector._pseudoElements; |
|
1426 }, |
|
1427 |
|
1428 /** |
|
1429 * Retrieve specificity information for the current selector. |
|
1430 * |
|
1431 * @see http://www.w3.org/TR/css3-selectors/#specificity |
|
1432 * @see http://www.w3.org/TR/CSS2/selector.html |
|
1433 * |
|
1434 * @return {Number} The selector's specificity. |
|
1435 */ |
|
1436 get specificity() |
|
1437 { |
|
1438 if (this._specificity) { |
|
1439 return this._specificity; |
|
1440 } |
|
1441 |
|
1442 this._specificity = domUtils.getSpecificity(this.cssRule.domRule, |
|
1443 this.selectorIndex); |
|
1444 |
|
1445 return this._specificity; |
|
1446 }, |
|
1447 |
|
1448 toString: function CssSelector_toString() |
|
1449 { |
|
1450 return this.text; |
|
1451 }, |
|
1452 }; |
|
1453 |
|
1454 /** |
|
1455 * A cache of information about the matched rules, selectors and values attached |
|
1456 * to a CSS property, for the highlighted element. |
|
1457 * |
|
1458 * The heart of the CssPropertyInfo object is the _findMatchedSelectors() |
|
1459 * method. This are invoked when the PropertyView tries to access the |
|
1460 * .matchedSelectors array. |
|
1461 * Results are cached, for later reuse. |
|
1462 * |
|
1463 * @param {CssLogic} aCssLogic Reference to the parent CssLogic instance |
|
1464 * @param {string} aProperty The CSS property we are gathering information for |
|
1465 * @constructor |
|
1466 */ |
|
1467 function CssPropertyInfo(aCssLogic, aProperty) |
|
1468 { |
|
1469 this._cssLogic = aCssLogic; |
|
1470 this.property = aProperty; |
|
1471 this._value = ""; |
|
1472 |
|
1473 // The number of matched rules holding the this.property style property. |
|
1474 // Additionally, only rules that come from allowed stylesheets are counted. |
|
1475 this._matchedRuleCount = 0; |
|
1476 |
|
1477 // An array holding CssSelectorInfo objects for each of the matched selectors |
|
1478 // that are inside a CSS rule. Only rules that hold the this.property are |
|
1479 // counted. This includes rules that come from filtered stylesheets (those |
|
1480 // that have sheetAllowed = false). |
|
1481 this._matchedSelectors = null; |
|
1482 } |
|
1483 |
|
1484 CssPropertyInfo.prototype = { |
|
1485 /** |
|
1486 * Retrieve the computed style value for the current property, for the |
|
1487 * highlighted element. |
|
1488 * |
|
1489 * @return {string} the computed style value for the current property, for the |
|
1490 * highlighted element. |
|
1491 */ |
|
1492 get value() |
|
1493 { |
|
1494 if (!this._value && this._cssLogic._computedStyle) { |
|
1495 try { |
|
1496 this._value = this._cssLogic._computedStyle.getPropertyValue(this.property); |
|
1497 } catch (ex) { |
|
1498 Services.console.logStringMessage('Error reading computed style for ' + |
|
1499 this.property); |
|
1500 Services.console.logStringMessage(ex); |
|
1501 } |
|
1502 } |
|
1503 return this._value; |
|
1504 }, |
|
1505 |
|
1506 /** |
|
1507 * Retrieve the number of matched rules holding the this.property style |
|
1508 * property. Only rules that come from allowed stylesheets are counted. |
|
1509 * |
|
1510 * @return {number} the number of matched rules. |
|
1511 */ |
|
1512 get matchedRuleCount() |
|
1513 { |
|
1514 if (!this._matchedSelectors) { |
|
1515 this._findMatchedSelectors(); |
|
1516 } else if (this.needRefilter) { |
|
1517 this._refilterSelectors(); |
|
1518 } |
|
1519 |
|
1520 return this._matchedRuleCount; |
|
1521 }, |
|
1522 |
|
1523 /** |
|
1524 * Retrieve the array holding CssSelectorInfo objects for each of the matched |
|
1525 * selectors, from each of the matched rules. Only selectors coming from |
|
1526 * allowed stylesheets are included in the array. |
|
1527 * |
|
1528 * @return {array} the list of CssSelectorInfo objects of selectors that match |
|
1529 * the highlighted element and its parents. |
|
1530 */ |
|
1531 get matchedSelectors() |
|
1532 { |
|
1533 if (!this._matchedSelectors) { |
|
1534 this._findMatchedSelectors(); |
|
1535 } else if (this.needRefilter) { |
|
1536 this._refilterSelectors(); |
|
1537 } |
|
1538 |
|
1539 return this._matchedSelectors; |
|
1540 }, |
|
1541 |
|
1542 /** |
|
1543 * Find the selectors that match the highlighted element and its parents. |
|
1544 * Uses CssLogic.processMatchedSelectors() to find the matched selectors, |
|
1545 * passing in a reference to CssPropertyInfo._processMatchedSelector() to |
|
1546 * create CssSelectorInfo objects, which we then sort |
|
1547 * @private |
|
1548 */ |
|
1549 _findMatchedSelectors: function CssPropertyInfo_findMatchedSelectors() |
|
1550 { |
|
1551 this._matchedSelectors = []; |
|
1552 this._matchedRuleCount = 0; |
|
1553 this.needRefilter = false; |
|
1554 |
|
1555 this._cssLogic.processMatchedSelectors(this._processMatchedSelector, this); |
|
1556 |
|
1557 // Sort the selectors by how well they match the given element. |
|
1558 this._matchedSelectors.sort(function(aSelectorInfo1, aSelectorInfo2) { |
|
1559 if (aSelectorInfo1.status > aSelectorInfo2.status) { |
|
1560 return -1; |
|
1561 } else if (aSelectorInfo2.status > aSelectorInfo1.status) { |
|
1562 return 1; |
|
1563 } else { |
|
1564 return aSelectorInfo1.compareTo(aSelectorInfo2); |
|
1565 } |
|
1566 }); |
|
1567 |
|
1568 // Now we know which of the matches is best, we can mark it BEST_MATCH. |
|
1569 if (this._matchedSelectors.length > 0 && |
|
1570 this._matchedSelectors[0].status > CssLogic.STATUS.UNMATCHED) { |
|
1571 this._matchedSelectors[0].status = CssLogic.STATUS.BEST; |
|
1572 } |
|
1573 }, |
|
1574 |
|
1575 /** |
|
1576 * Process a matched CssSelector object. |
|
1577 * |
|
1578 * @private |
|
1579 * @param {CssSelector} aSelector the matched CssSelector object. |
|
1580 * @param {CssLogic.STATUS} aStatus the CssSelector match status. |
|
1581 */ |
|
1582 _processMatchedSelector: function CssPropertyInfo_processMatchedSelector(aSelector, aStatus) |
|
1583 { |
|
1584 let cssRule = aSelector.cssRule; |
|
1585 let value = cssRule.getPropertyValue(this.property); |
|
1586 if (value && |
|
1587 (aStatus == CssLogic.STATUS.MATCHED || |
|
1588 (aStatus == CssLogic.STATUS.PARENT_MATCH && |
|
1589 domUtils.isInheritedProperty(this.property)))) { |
|
1590 let selectorInfo = new CssSelectorInfo(aSelector, this.property, value, |
|
1591 aStatus); |
|
1592 this._matchedSelectors.push(selectorInfo); |
|
1593 if (this._cssLogic._passId !== cssRule._passId && cssRule.sheetAllowed) { |
|
1594 this._matchedRuleCount++; |
|
1595 } |
|
1596 } |
|
1597 }, |
|
1598 |
|
1599 /** |
|
1600 * Refilter the matched selectors array when the CssLogic.sourceFilter |
|
1601 * changes. This allows for quick filter changes. |
|
1602 * @private |
|
1603 */ |
|
1604 _refilterSelectors: function CssPropertyInfo_refilterSelectors() |
|
1605 { |
|
1606 let passId = ++this._cssLogic._passId; |
|
1607 let ruleCount = 0; |
|
1608 |
|
1609 let iterator = function(aSelectorInfo) { |
|
1610 let cssRule = aSelectorInfo.selector.cssRule; |
|
1611 if (cssRule._passId != passId) { |
|
1612 if (cssRule.sheetAllowed) { |
|
1613 ruleCount++; |
|
1614 } |
|
1615 cssRule._passId = passId; |
|
1616 } |
|
1617 }; |
|
1618 |
|
1619 if (this._matchedSelectors) { |
|
1620 this._matchedSelectors.forEach(iterator); |
|
1621 this._matchedRuleCount = ruleCount; |
|
1622 } |
|
1623 |
|
1624 this.needRefilter = false; |
|
1625 }, |
|
1626 |
|
1627 toString: function CssPropertyInfo_toString() |
|
1628 { |
|
1629 return "CssPropertyInfo[" + this.property + "]"; |
|
1630 }, |
|
1631 }; |
|
1632 |
|
1633 /** |
|
1634 * A class that holds information about a given CssSelector object. |
|
1635 * |
|
1636 * Instances of this class are given to CssHtmlTree in the array of matched |
|
1637 * selectors. Each such object represents a displayable row in the PropertyView |
|
1638 * objects. The information given by this object blends data coming from the |
|
1639 * CssSheet, CssRule and from the CssSelector that own this object. |
|
1640 * |
|
1641 * @param {CssSelector} aSelector The CssSelector object for which to present information. |
|
1642 * @param {string} aProperty The property for which information should be retrieved. |
|
1643 * @param {string} aValue The property value from the CssRule that owns the selector. |
|
1644 * @param {CssLogic.STATUS} aStatus The selector match status. |
|
1645 * @constructor |
|
1646 */ |
|
1647 function CssSelectorInfo(aSelector, aProperty, aValue, aStatus) |
|
1648 { |
|
1649 this.selector = aSelector; |
|
1650 this.property = aProperty; |
|
1651 this.status = aStatus; |
|
1652 this.value = aValue; |
|
1653 let priority = this.selector.cssRule.getPropertyPriority(this.property); |
|
1654 this.important = (priority === "important"); |
|
1655 } |
|
1656 |
|
1657 CssSelectorInfo.prototype = { |
|
1658 /** |
|
1659 * Retrieve the CssSelector source, which is the source of the CssSheet owning |
|
1660 * the selector. |
|
1661 * |
|
1662 * @return {string} the selector source. |
|
1663 */ |
|
1664 get source() |
|
1665 { |
|
1666 return this.selector.source; |
|
1667 }, |
|
1668 |
|
1669 /** |
|
1670 * Retrieve the CssSelector source element, which is the source of the CssRule |
|
1671 * owning the selector. This is only available when the CssSelector comes from |
|
1672 * an element.style. |
|
1673 * |
|
1674 * @return {string} the source element selector. |
|
1675 */ |
|
1676 get sourceElement() |
|
1677 { |
|
1678 return this.selector.sourceElement; |
|
1679 }, |
|
1680 |
|
1681 /** |
|
1682 * Retrieve the address of the CssSelector. This points to the address of the |
|
1683 * CssSheet owning this selector. |
|
1684 * |
|
1685 * @return {string} the address of the CssSelector. |
|
1686 */ |
|
1687 get href() |
|
1688 { |
|
1689 return this.selector.href; |
|
1690 }, |
|
1691 |
|
1692 /** |
|
1693 * Check if the CssSelector comes from element.style or not. |
|
1694 * |
|
1695 * @return {boolean} true if the CssSelector comes from element.style, or |
|
1696 * false otherwise. |
|
1697 */ |
|
1698 get elementStyle() |
|
1699 { |
|
1700 return this.selector.elementStyle; |
|
1701 }, |
|
1702 |
|
1703 /** |
|
1704 * Retrieve specificity information for the current selector. |
|
1705 * |
|
1706 * @return {object} an object holding specificity information for the current |
|
1707 * selector. |
|
1708 */ |
|
1709 get specificity() |
|
1710 { |
|
1711 return this.selector.specificity; |
|
1712 }, |
|
1713 |
|
1714 /** |
|
1715 * Retrieve the parent stylesheet index/position in the viewed document. |
|
1716 * |
|
1717 * @return {number} the parent stylesheet index/position in the viewed |
|
1718 * document. |
|
1719 */ |
|
1720 get sheetIndex() |
|
1721 { |
|
1722 return this.selector.sheetIndex; |
|
1723 }, |
|
1724 |
|
1725 /** |
|
1726 * Check if the parent stylesheet is allowed by the CssLogic.sourceFilter. |
|
1727 * |
|
1728 * @return {boolean} true if the parent stylesheet is allowed by the current |
|
1729 * sourceFilter, or false otherwise. |
|
1730 */ |
|
1731 get sheetAllowed() |
|
1732 { |
|
1733 return this.selector.sheetAllowed; |
|
1734 }, |
|
1735 |
|
1736 /** |
|
1737 * Retrieve the line of the parent CSSStyleRule in the parent CSSStyleSheet. |
|
1738 * |
|
1739 * @return {number} the line of the parent CSSStyleRule in the parent |
|
1740 * stylesheet. |
|
1741 */ |
|
1742 get ruleLine() |
|
1743 { |
|
1744 return this.selector.ruleLine; |
|
1745 }, |
|
1746 |
|
1747 /** |
|
1748 * Check if the selector comes from a browser-provided stylesheet. |
|
1749 * |
|
1750 * @return {boolean} true if the selector comes from a browser-provided |
|
1751 * stylesheet, or false otherwise. |
|
1752 */ |
|
1753 get contentRule() |
|
1754 { |
|
1755 return this.selector.contentRule; |
|
1756 }, |
|
1757 |
|
1758 /** |
|
1759 * Compare the current CssSelectorInfo instance to another instance, based on |
|
1760 * specificity information. |
|
1761 * |
|
1762 * @param {CssSelectorInfo} aThat The instance to compare ourselves against. |
|
1763 * @return number -1, 0, 1 depending on how aThat compares with this. |
|
1764 */ |
|
1765 compareTo: function CssSelectorInfo_compareTo(aThat) |
|
1766 { |
|
1767 if (!this.contentRule && aThat.contentRule) return 1; |
|
1768 if (this.contentRule && !aThat.contentRule) return -1; |
|
1769 |
|
1770 if (this.elementStyle && !aThat.elementStyle) { |
|
1771 if (!this.important && aThat.important) return 1; |
|
1772 else return -1; |
|
1773 } |
|
1774 |
|
1775 if (!this.elementStyle && aThat.elementStyle) { |
|
1776 if (this.important && !aThat.important) return -1; |
|
1777 else return 1; |
|
1778 } |
|
1779 |
|
1780 if (this.important && !aThat.important) return -1; |
|
1781 if (aThat.important && !this.important) return 1; |
|
1782 |
|
1783 if (this.specificity > aThat.specificity) return -1; |
|
1784 if (aThat.specificity > this.specificity) return 1; |
|
1785 |
|
1786 if (this.sheetIndex > aThat.sheetIndex) return -1; |
|
1787 if (aThat.sheetIndex > this.sheetIndex) return 1; |
|
1788 |
|
1789 if (this.ruleLine > aThat.ruleLine) return -1; |
|
1790 if (aThat.ruleLine > this.ruleLine) return 1; |
|
1791 |
|
1792 return 0; |
|
1793 }, |
|
1794 |
|
1795 toString: function CssSelectorInfo_toString() |
|
1796 { |
|
1797 return this.selector + " -> " + this.value; |
|
1798 }, |
|
1799 }; |
|
1800 |
|
1801 XPCOMUtils.defineLazyGetter(this, "domUtils", function() { |
|
1802 return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); |
|
1803 }); |