mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
859 lines
25 KiB
JavaScript
859 lines
25 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
const { Cc, Ci, Cu } = require("chrome");
|
|
|
|
const Services = require("Services");
|
|
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
const events = require("sdk/event/core");
|
|
const protocol = require("devtools/server/protocol");
|
|
const { method, custom, RetVal, Arg } = protocol;
|
|
|
|
loader.lazyGetter(this, "gDevTools", () => {
|
|
return require("resource:///modules/devtools/gDevTools.jsm").gDevTools;
|
|
});
|
|
loader.lazyGetter(this, "DOMUtils", () => {
|
|
return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils)
|
|
});
|
|
loader.lazyGetter(this, "stylesheets", () => {
|
|
return require("devtools/server/actors/stylesheets");
|
|
});
|
|
loader.lazyGetter(this, "CssLogic", () => {
|
|
return require("devtools/styleinspector/css-logic").CssLogic;
|
|
});
|
|
|
|
const CSSRule = Ci.nsIDOMCSSRule;
|
|
|
|
const MAX_UNUSED_RULES = 10000;
|
|
|
|
/**
|
|
* Allow: let foo = l10n.lookup("csscoverageFoo");
|
|
*/
|
|
const l10n = exports.l10n = {
|
|
_URI: "chrome://global/locale/devtools/csscoverage.properties",
|
|
lookup: function(msg) {
|
|
if (this._stringBundle == null) {
|
|
this._stringBundle = Services.strings.createBundle(this._URI);
|
|
}
|
|
return this._stringBundle.GetStringFromName(msg);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* CSSUsage manages the collection of CSS usage data.
|
|
* The core of a CSSUsage is a JSON-able data structure called _knownRules
|
|
* which looks like this:
|
|
* This records the CSSStyleRules and their usage.
|
|
* The format is:
|
|
* Map({
|
|
* <CSS-URL>|<START-LINE>|<START-COLUMN>: {
|
|
* selectorText: <CSSStyleRule.selectorText>,
|
|
* test: <simplify(CSSStyleRule.selectorText)>,
|
|
* cssText: <CSSStyleRule.cssText>,
|
|
* isUsed: <TRUE|FALSE>,
|
|
* presentOn: Set([ <HTML-URL>, ... ]),
|
|
* preLoadOn: Set([ <HTML-URL>, ... ]),
|
|
* isError: <TRUE|FALSE>,
|
|
* }
|
|
* })
|
|
*
|
|
* For example:
|
|
* this._knownRules = Map({
|
|
* "http://eg.com/styles1.css|15|0": {
|
|
* selectorText: "p.quote:hover",
|
|
* test: "p.quote",
|
|
* cssText: "p.quote { color: red; }",
|
|
* isUsed: true,
|
|
* presentOn: Set([ "http://eg.com/page1.html", ... ]),
|
|
* preLoadOn: Set([ "http://eg.com/page1.html" ]),
|
|
* isError: false,
|
|
* }, ...
|
|
* });
|
|
*/
|
|
let CSSUsageActor = protocol.ActorClass({
|
|
typeName: "cssUsage",
|
|
|
|
events: {
|
|
"state-change" : {
|
|
type: "stateChange",
|
|
stateChange: Arg(0, "json")
|
|
}
|
|
},
|
|
|
|
initialize: function(conn, tabActor) {
|
|
protocol.Actor.prototype.initialize.call(this, conn);
|
|
|
|
this._tabActor = tabActor;
|
|
this._running = false;
|
|
|
|
this._onTabLoad = this._onTabLoad.bind(this);
|
|
this._onChange = this._onChange.bind(this);
|
|
|
|
this._notifyOn = Ci.nsIWebProgress.NOTIFY_STATUS |
|
|
Ci.nsIWebProgress.NOTIFY_STATE_ALL
|
|
},
|
|
|
|
destroy: function() {
|
|
this._tabActor = undefined;
|
|
|
|
delete this._onTabLoad;
|
|
delete this._onChange;
|
|
|
|
protocol.Actor.prototype.destroy.call(this);
|
|
},
|
|
|
|
/**
|
|
* Begin recording usage data
|
|
* @param noreload It's best if we start by reloading the current page
|
|
* because that starts the test at a known point, but there could be reasons
|
|
* why we don't want to do that (e.g. the page contains state that will be
|
|
* lost across a reload)
|
|
*/
|
|
start: method(function(noreload) {
|
|
if (this._running) {
|
|
throw new Error(l10n.lookup("csscoverageRunningError"));
|
|
}
|
|
|
|
this._isOneShot = false;
|
|
this._visitedPages = new Set();
|
|
this._knownRules = new Map();
|
|
this._running = true;
|
|
this._tooManyUnused = false;
|
|
|
|
this._progressListener = {
|
|
QueryInterface: XPCOMUtils.generateQI([ Ci.nsIWebProgressListener,
|
|
Ci.nsISupportsWeakReference ]),
|
|
|
|
onStateChange: (progress, request, flags, status) => {
|
|
let isStop = flags & Ci.nsIWebProgressListener.STATE_STOP;
|
|
let isWindow = flags & Ci.nsIWebProgressListener.STATE_IS_WINDOW;
|
|
|
|
if (isStop && isWindow) {
|
|
this._onTabLoad(progress.DOMWindow.document);
|
|
}
|
|
},
|
|
|
|
onLocationChange: () => {},
|
|
onProgressChange: () => {},
|
|
onSecurityChange: () => {},
|
|
onStatusChange: () => {},
|
|
destroy: () => {}
|
|
};
|
|
|
|
this._progress = this._tabActor.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIWebProgress);
|
|
this._progress.addProgressListener(this._progressListener, this._notifyOn);
|
|
|
|
if (noreload) {
|
|
// If we're not starting by reloading the page, then pretend that onload
|
|
// has just happened.
|
|
this._onTabLoad(this._tabActor.window.document);
|
|
}
|
|
else {
|
|
this._tabActor.window.location.reload();
|
|
}
|
|
|
|
events.emit(this, "state-change", { isRunning: true });
|
|
}, {
|
|
request: { url: Arg(0, "boolean") }
|
|
}),
|
|
|
|
/**
|
|
* Cease recording usage data
|
|
*/
|
|
stop: method(function() {
|
|
if (!this._running) {
|
|
throw new Error(l10n.lookup("csscoverageNotRunningError"));
|
|
}
|
|
|
|
this._progress.removeProgressListener(this._progressListener, this._notifyOn);
|
|
this._progress = undefined;
|
|
|
|
this._running = false;
|
|
events.emit(this, "state-change", { isRunning: false });
|
|
}),
|
|
|
|
/**
|
|
* Start/stop recording usage data depending on what we're currently doing.
|
|
*/
|
|
toggle: method(function() {
|
|
return this._running ? this.stop() : this.start();
|
|
}),
|
|
|
|
/**
|
|
* Running start() quickly followed by stop() does a bunch of unnecessary
|
|
* work, so this cuts all that out
|
|
*/
|
|
oneshot: method(function() {
|
|
if (this._running) {
|
|
throw new Error(l10n.lookup("csscoverageRunningError"));
|
|
}
|
|
|
|
this._isOneShot = true;
|
|
this._visitedPages = new Set();
|
|
this._knownRules = new Map();
|
|
|
|
this._populateKnownRules(this._tabActor.window.document);
|
|
this._updateUsage(this._tabActor.window.document, false);
|
|
}),
|
|
|
|
/**
|
|
* Called by the ProgressListener to simulate a "load" event
|
|
*/
|
|
_onTabLoad: function(document) {
|
|
this._populateKnownRules(document);
|
|
this._updateUsage(document, true);
|
|
|
|
this._observeMutations(document);
|
|
},
|
|
|
|
/**
|
|
* Setup a MutationObserver on the current document
|
|
*/
|
|
_observeMutations: function(document) {
|
|
let MutationObserver = document.defaultView.MutationObserver;
|
|
let observer = new MutationObserver(mutations => {
|
|
// It's possible that one of the mutations in this list adds a 'use' of
|
|
// a CSS rule, and another takes it away. See Bug 1010189
|
|
this._onChange(document);
|
|
});
|
|
|
|
observer.observe(document, {
|
|
attributes: true,
|
|
childList: true,
|
|
characterData: false,
|
|
subtree: true
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Event handler for whenever we think the page has changed in a way that
|
|
* means the CSS usage might have changed.
|
|
*/
|
|
_onChange: function(document) {
|
|
// Ignore changes pre 'load'
|
|
if (!this._visitedPages.has(getURL(document))) {
|
|
return;
|
|
}
|
|
this._updateUsage(document, false);
|
|
},
|
|
|
|
/**
|
|
* Called whenever we think the list of stylesheets might have changed so
|
|
* we can update the list of rules that we should be checking
|
|
*/
|
|
_populateKnownRules: function(document) {
|
|
let url = getURL(document);
|
|
this._visitedPages.add(url);
|
|
// Go through all the rules in the current sheets adding them to knownRules
|
|
// if needed and adding the current url to the list of pages they're on
|
|
for (let rule of getAllSelectorRules(document)) {
|
|
let ruleId = ruleToId(rule);
|
|
let ruleData = this._knownRules.get(ruleId);
|
|
if (ruleData == null) {
|
|
ruleData = {
|
|
selectorText: rule.selectorText,
|
|
cssText: rule.cssText,
|
|
test: getTestSelector(rule.selectorText),
|
|
isUsed: false,
|
|
presentOn: new Set(),
|
|
preLoadOn: new Set(),
|
|
isError: false
|
|
};
|
|
this._knownRules.set(ruleId, ruleData);
|
|
}
|
|
|
|
ruleData.presentOn.add(url);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Update knownRules with usage information from the current page
|
|
*/
|
|
_updateUsage: function(document, isLoad) {
|
|
let qsaCount = 0;
|
|
|
|
// Update this._data with matches to say 'used at load time' by sheet X
|
|
let url = getURL(document);
|
|
|
|
for (let [ , ruleData ] of this._knownRules) {
|
|
// If it broke before, don't try again selectors don't change
|
|
if (ruleData.isError) {
|
|
continue;
|
|
}
|
|
|
|
// If it's used somewhere already, don't bother checking again unless
|
|
// this is a load event in which case we need to add preLoadOn
|
|
if (!isLoad && ruleData.isUsed) {
|
|
continue;
|
|
}
|
|
|
|
// Ignore rules that are not present on this page
|
|
if (!ruleData.presentOn.has(url)) {
|
|
continue;
|
|
}
|
|
|
|
qsaCount++;
|
|
if (qsaCount > MAX_UNUSED_RULES) {
|
|
console.error("Too many unused rules on " + url + " ");
|
|
this._tooManyUnused = true;
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
let match = document.querySelector(ruleData.test);
|
|
if (match != null) {
|
|
ruleData.isUsed = true;
|
|
if (isLoad) {
|
|
ruleData.preLoadOn.add(url);
|
|
}
|
|
}
|
|
}
|
|
catch (ex) {
|
|
ruleData.isError = true;
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns a JSONable structure designed to help marking up the style editor,
|
|
* which describes the CSS selector usage.
|
|
* Example:
|
|
* [
|
|
* {
|
|
* selectorText: "p#content",
|
|
* usage: "unused|used",
|
|
* start: { line: 3, column: 0 },
|
|
* },
|
|
* ...
|
|
* ]
|
|
*/
|
|
createEditorReport: method(function(url) {
|
|
if (this._knownRules == null) {
|
|
return { reports: [] };
|
|
}
|
|
|
|
let reports = [];
|
|
for (let [ruleId, ruleData] of this._knownRules) {
|
|
let { url: ruleUrl, line, column } = deconstructRuleId(ruleId);
|
|
if (ruleUrl !== url || ruleData.isUsed) {
|
|
continue;
|
|
}
|
|
|
|
let ruleReport = {
|
|
selectorText: ruleData.selectorText,
|
|
start: { line: line, column: column }
|
|
};
|
|
|
|
if (ruleData.end) {
|
|
ruleReport.end = ruleData.end;
|
|
}
|
|
|
|
reports.push(ruleReport);
|
|
}
|
|
|
|
return { reports: reports };
|
|
}, {
|
|
request: { url: Arg(0, "string") },
|
|
response: { reports: RetVal("array:json") }
|
|
}),
|
|
|
|
/**
|
|
* Returns a JSONable structure designed for the page report which shows
|
|
* the recommended changes to a page.
|
|
*
|
|
* "preload" means that a rule is used before the load event happens, which
|
|
* means that the page could by optimized by placing it in a <style> element
|
|
* at the top of the page, moving the <link> elements to the bottom.
|
|
*
|
|
* Example:
|
|
* {
|
|
* preload: [
|
|
* {
|
|
* url: "http://example.org/page1.html",
|
|
* shortUrl: "page1.html",
|
|
* rules: [
|
|
* {
|
|
* url: "http://example.org/style1.css",
|
|
* shortUrl: "style1.css",
|
|
* start: { line: 3, column: 4 },
|
|
* selectorText: "p#content",
|
|
* formattedCssText: "p#content {\n color: red;\n }\n"
|
|
* },
|
|
* ...
|
|
* ]
|
|
* }
|
|
* ],
|
|
* unused: [
|
|
* {
|
|
* url: "http://example.org/style1.css",
|
|
* shortUrl: "style1.css",
|
|
* rules: [ ... ]
|
|
* }
|
|
* ]
|
|
* }
|
|
*/
|
|
createPageReport: method(function() {
|
|
if (this._running) {
|
|
throw new Error(l10n.lookup("csscoverageRunningError"));
|
|
}
|
|
|
|
if (this._visitedPages == null) {
|
|
throw new Error(l10n.lookup("csscoverageNotRunError"));
|
|
}
|
|
|
|
if (this._isOneShot) {
|
|
throw new Error(l10n.lookup("csscoverageOneShotReportError"));
|
|
}
|
|
|
|
// Helper function to create a JSONable data structure representing a rule
|
|
const ruleToRuleReport = function(rule, ruleData) {
|
|
return {
|
|
url: rule.url,
|
|
shortUrl: rule.url.split("/").slice(-1)[0],
|
|
start: { line: rule.line, column: rule.column },
|
|
selectorText: ruleData.selectorText,
|
|
formattedCssText: CssLogic.prettifyCSS(ruleData.cssText)
|
|
};
|
|
}
|
|
|
|
// A count of each type of rule for the bar chart
|
|
let summary = { used: 0, unused: 0, preload: 0 };
|
|
|
|
// Create the set of the unused rules
|
|
let unusedMap = new Map();
|
|
for (let [ruleId, ruleData] of this._knownRules) {
|
|
let rule = deconstructRuleId(ruleId);
|
|
let rules = unusedMap.get(rule.url)
|
|
if (rules == null) {
|
|
rules = [];
|
|
unusedMap.set(rule.url, rules);
|
|
}
|
|
if (!ruleData.isUsed) {
|
|
let ruleReport = ruleToRuleReport(rule, ruleData);
|
|
rules.push(ruleReport);
|
|
}
|
|
else {
|
|
summary.unused++;
|
|
}
|
|
}
|
|
let unused = [];
|
|
for (let [url, rules] of unusedMap) {
|
|
unused.push({
|
|
url: url,
|
|
shortUrl: url.split("/").slice(-1),
|
|
rules: rules
|
|
});
|
|
}
|
|
|
|
// Create the set of rules that could be pre-loaded
|
|
let preload = [];
|
|
for (let url of this._visitedPages) {
|
|
let page = {
|
|
url: url,
|
|
shortUrl: url.split("/").slice(-1),
|
|
rules: []
|
|
};
|
|
|
|
for (let [ruleId, ruleData] of this._knownRules) {
|
|
if (ruleData.preLoadOn.has(url)) {
|
|
let rule = deconstructRuleId(ruleId);
|
|
let ruleReport = ruleToRuleReport(rule, ruleData);
|
|
page.rules.push(ruleReport);
|
|
summary.preload++;
|
|
}
|
|
else {
|
|
summary.used++;
|
|
}
|
|
}
|
|
|
|
if (page.rules.length > 0) {
|
|
preload.push(page);
|
|
}
|
|
}
|
|
|
|
return {
|
|
summary: summary,
|
|
preload: preload,
|
|
unused: unused
|
|
};
|
|
}, {
|
|
response: RetVal("json")
|
|
}),
|
|
|
|
/**
|
|
* For testing only. What pages did we visit.
|
|
*/
|
|
_testOnly_visitedPages: method(function() {
|
|
return [...this._visitedPages];
|
|
}, {
|
|
response: { value: RetVal("array:string") }
|
|
}),
|
|
});
|
|
|
|
exports.CSSUsageActor = CSSUsageActor;
|
|
|
|
/**
|
|
* Generator that filters the CSSRules out of _getAllRules so it only
|
|
* iterates over the CSSStyleRules
|
|
*/
|
|
function* getAllSelectorRules(document) {
|
|
for (let rule of getAllRules(document)) {
|
|
if (rule.type === CSSRule.STYLE_RULE && rule.selectorText !== "") {
|
|
yield rule;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generator to iterate over the CSSRules in all the stylesheets the
|
|
* current document (i.e. it includes import rules, media rules, etc)
|
|
*/
|
|
function* getAllRules(document) {
|
|
// sheets is an array of the <link> and <style> element in this document
|
|
let sheets = getAllSheets(document);
|
|
for (let i = 0; i < sheets.length; i++) {
|
|
for (let j = 0; j < sheets[i].cssRules.length; j++) {
|
|
yield sheets[i].cssRules[j];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get an array of all the stylesheets that affect this document. That means
|
|
* the <link> and <style> based sheets, and the @imported sheets (recursively)
|
|
* but not the sheets in nested frames.
|
|
*/
|
|
function getAllSheets(document) {
|
|
// sheets is an array of the <link> and <style> element in this document
|
|
let sheets = Array.slice(document.styleSheets);
|
|
// Add @imported sheets
|
|
for (let i = 0; i < sheets.length; i++) {
|
|
let subSheets = getImportedSheets(sheets[i]);
|
|
sheets = sheets.concat(...subSheets);
|
|
}
|
|
return sheets;
|
|
}
|
|
|
|
/**
|
|
* Recursively find @import rules in the given stylesheet.
|
|
* We're relying on the browser giving rule.styleSheet == null to resolve
|
|
* @import loops
|
|
*/
|
|
function getImportedSheets(stylesheet) {
|
|
let sheets = [];
|
|
for (let i = 0; i < stylesheet.cssRules.length; i++) {
|
|
let rule = stylesheet.cssRules[i];
|
|
// rule.styleSheet == null with duplicate @imports for the same URL.
|
|
if (rule.type === CSSRule.IMPORT_RULE && rule.styleSheet != null) {
|
|
sheets.push(rule.styleSheet);
|
|
let subSheets = getImportedSheets(rule.styleSheet);
|
|
sheets = sheets.concat(...subSheets);
|
|
}
|
|
}
|
|
return sheets;
|
|
}
|
|
|
|
/**
|
|
* Get a unique identifier for a rule. This is currently the string
|
|
* <CSS-URL>|<START-LINE>|<START-COLUMN>
|
|
* @see deconstructRuleId(ruleId)
|
|
*/
|
|
function ruleToId(rule) {
|
|
let loc = stylesheets.getRuleLocation(rule);
|
|
return sheetToUrl(rule.parentStyleSheet) + "|" + loc.line + "|" + loc.column;
|
|
}
|
|
|
|
/**
|
|
* Convert a ruleId to an object with { url, line, column } properties
|
|
* @see ruleToId(rule)
|
|
*/
|
|
const deconstructRuleId = exports.deconstructRuleId = function(ruleId) {
|
|
let split = ruleId.split("|");
|
|
if (split.length > 3) {
|
|
let replace = split.slice(0, split.length - 3 + 1).join("|");
|
|
split.splice(0, split.length - 3 + 1, replace);
|
|
}
|
|
let [ url, line, column ] = split;
|
|
return {
|
|
url: url,
|
|
line: parseInt(line, 10),
|
|
column: parseInt(column, 10)
|
|
};
|
|
};
|
|
|
|
/**
|
|
* We're only interested in the origin and pathname, because changes to the
|
|
* username, password, hash, or query string probably don't significantly
|
|
* change the CSS usage properties of a page.
|
|
* @param document
|
|
*/
|
|
const getURL = exports.getURL = function(document) {
|
|
let url = new document.defaultView.URL(document.documentURI);
|
|
return '' + url.origin + url.pathname;
|
|
};
|
|
|
|
/**
|
|
* Pseudo class handling constants:
|
|
* We split pseudo-classes into a number of categories so we can decide how we
|
|
* should match them. See getTestSelector for how we use these constants.
|
|
*
|
|
* @see http://dev.w3.org/csswg/selectors4/#overview
|
|
* @see https://developer.mozilla.org/en-US/docs/tag/CSS%20Pseudo-class
|
|
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements
|
|
*/
|
|
|
|
/**
|
|
* Category 1: Pseudo-classes that depend on external browser/OS state
|
|
* This includes things like the time, locale, position of mouse/caret/window,
|
|
* contents of browser history, etc. These can be hard to mimic.
|
|
* Action: Remove from selectors
|
|
*/
|
|
const SEL_EXTERNAL = [
|
|
"active", "active-drop", "current", "dir", "focus", "future", "hover",
|
|
"invalid-drop", "lang", "past", "placeholder-shown", "target", "valid-drop",
|
|
"visited"
|
|
];
|
|
|
|
/**
|
|
* Category 2: Pseudo-classes that depend on user-input state
|
|
* These are pseudo-classes that arguably *should* be covered by unit tests but
|
|
* which probably aren't and which are unlikely to be covered by manual tests.
|
|
* We're currently stripping them out,
|
|
* Action: Remove from selectors (but consider future command line flag to
|
|
* enable them in the future. e.g. 'csscoverage start --strict')
|
|
*/
|
|
const SEL_FORM = [
|
|
"checked", "default", "disabled", "enabled", "fullscreen", "in-range",
|
|
"indeterminate", "invalid", "optional", "out-of-range", "required", "valid"
|
|
];
|
|
|
|
/**
|
|
* Category 3: Pseudo-elements
|
|
* querySelectorAll doesn't return matches with pseudo-elements because there
|
|
* is no element to match (they're pseudo) so we have to remove them all.
|
|
* (See http://codepen.io/joewalker/pen/sanDw for a demo)
|
|
* Action: Remove from selectors (including deprecated single colon versions)
|
|
*/
|
|
const SEL_ELEMENT = [
|
|
"after", "before", "first-letter", "first-line", "selection"
|
|
];
|
|
|
|
/**
|
|
* Category 4: Structural pseudo-classes
|
|
* This is a category defined by the spec (also called tree-structural and
|
|
* grid-structural) for selection based on relative position in the document
|
|
* tree that cannot be represented by other simple selectors or combinators.
|
|
* Action: Require a page-match
|
|
*/
|
|
const SEL_STRUCTURAL = [
|
|
"empty", "first-child", "first-of-type", "last-child", "last-of-type",
|
|
"nth-column", "nth-last-column", "nth-child", "nth-last-child",
|
|
"nth-last-of-type", "nth-of-type", "only-child", "only-of-type", "root"
|
|
];
|
|
|
|
/**
|
|
* Category 4a: Semi-structural pseudo-classes
|
|
* These are not structural according to the spec, but act nevertheless on
|
|
* information in the document tree.
|
|
* Action: Require a page-match
|
|
*/
|
|
const SEL_SEMI = [ "any-link", "link", "read-only", "read-write", "scope" ];
|
|
|
|
/**
|
|
* Category 5: Combining pseudo-classes
|
|
* has(), not() etc join selectors together in various ways. We take care when
|
|
* removing pseudo-classes to convert "not(:hover)" into "not(*)" and so on.
|
|
* With these changes the combining pseudo-classes should probably stand on
|
|
* their own.
|
|
* Action: Require a page-match
|
|
*/
|
|
const SEL_COMBINING = [ "not", "has", "matches" ];
|
|
|
|
/**
|
|
* Category 6: Media pseudo-classes
|
|
* Pseudo-classes that should be ignored because they're only relevant to
|
|
* media queries
|
|
* Action: Don't need removing from selectors as they appear in media queries
|
|
*/
|
|
const SEL_MEDIA = [ "blank", "first", "left", "right" ];
|
|
|
|
/**
|
|
* A test selector is a reduced form of a selector that we actually test
|
|
* against. This code strips out pseudo-elements and some pseudo-classes that
|
|
* we think should not have to match in order for the selector to be relevant.
|
|
*/
|
|
function getTestSelector(selector) {
|
|
let replacement = selector;
|
|
let replaceSelector = pseudo => {
|
|
replacement = replacement.replace(" :" + pseudo, " *")
|
|
.replace("(:" + pseudo, "(*")
|
|
.replace(":" + pseudo, "");
|
|
};
|
|
|
|
SEL_EXTERNAL.forEach(replaceSelector);
|
|
SEL_FORM.forEach(replaceSelector);
|
|
SEL_ELEMENT.forEach(replaceSelector);
|
|
|
|
// Pseudo elements work in : and :: forms
|
|
SEL_ELEMENT.forEach(pseudo => {
|
|
replacement = replacement.replace("::" + pseudo, "");
|
|
});
|
|
|
|
return replacement;
|
|
}
|
|
|
|
/**
|
|
* I've documented all known pseudo-classes above for 2 reasons: To allow
|
|
* checking logic and what might be missing, but also to allow a unit test
|
|
* that fetches the list of supported pseudo-classes and pseudo-elements from
|
|
* the platform and check that they were all represented here.
|
|
*/
|
|
exports.SEL_ALL = [
|
|
SEL_EXTERNAL, SEL_FORM, SEL_ELEMENT, SEL_STRUCTURAL, SEL_SEMI,
|
|
SEL_COMBINING, SEL_MEDIA
|
|
].reduce(function(prev, curr) { return prev.concat(curr); }, []);
|
|
|
|
/**
|
|
* Find a URL for a given stylesheet
|
|
*/
|
|
const sheetToUrl = exports.sheetToUrl = function(stylesheet) {
|
|
// For <link> elements
|
|
if (stylesheet.href) {
|
|
return stylesheet.href;
|
|
}
|
|
|
|
// For <style> elements
|
|
if (stylesheet.ownerNode) {
|
|
let document = stylesheet.ownerNode.ownerDocument;
|
|
let sheets = [...document.querySelectorAll("style")];
|
|
let index = sheets.indexOf(stylesheet.ownerNode);
|
|
return getURL(document) + ' → <style> index ' + index;
|
|
}
|
|
|
|
throw new Error("Unknown sheet source");
|
|
}
|
|
|
|
/**
|
|
* Running more than one usage report at a time is probably bad for performance
|
|
* and it isn't particularly useful, and it's confusing from a notification POV
|
|
* so we only allow one.
|
|
*/
|
|
let isRunning = false;
|
|
let notification;
|
|
let target;
|
|
let chromeWindow;
|
|
|
|
/**
|
|
* Front for CSSUsageActor
|
|
*/
|
|
const CSSUsageFront = protocol.FrontClass(CSSUsageActor, {
|
|
initialize: function(client, form) {
|
|
protocol.Front.prototype.initialize.call(this, client, form);
|
|
this.actorID = form.cssUsageActor;
|
|
this.manage(this);
|
|
},
|
|
|
|
_onStateChange: protocol.preEvent("state-change", function(ev) {
|
|
isRunning = ev.isRunning;
|
|
ev.target = target;
|
|
|
|
if (isRunning) {
|
|
let gnb = chromeWindow.document.getElementById("global-notificationbox");
|
|
notification = gnb.getNotificationWithValue("csscoverage-running");
|
|
|
|
if (notification == null) {
|
|
let notifyStop = reason => {
|
|
if (reason == "removed") {
|
|
this.stop();
|
|
}
|
|
};
|
|
|
|
let msg = l10n.lookup("csscoverageRunningReply");
|
|
notification = gnb.appendNotification(msg, "csscoverage-running",
|
|
"", // i.e. no image
|
|
gnb.PRIORITY_INFO_HIGH,
|
|
null, // i.e. no buttons
|
|
notifyStop);
|
|
}
|
|
}
|
|
else {
|
|
if (notification) {
|
|
notification.remove();
|
|
notification = undefined;
|
|
}
|
|
|
|
gDevTools.showToolbox(target, "styleeditor");
|
|
target = undefined;
|
|
}
|
|
}),
|
|
|
|
/**
|
|
* Server-side start is above. Client-side start adds a notification box
|
|
*/
|
|
start: custom(function(newChromeWindow, newTarget, noreload=false) {
|
|
target = newTarget;
|
|
chromeWindow = newChromeWindow;
|
|
|
|
return this._start(noreload);
|
|
}, {
|
|
impl: "_start"
|
|
}),
|
|
|
|
/**
|
|
* Server-side start is above. Client-side start adds a notification box
|
|
*/
|
|
toggle: custom(function(newChromeWindow, newTarget) {
|
|
target = newTarget;
|
|
chromeWindow = newChromeWindow;
|
|
|
|
return this._toggle();
|
|
}, {
|
|
impl: "_toggle"
|
|
}),
|
|
|
|
/**
|
|
* We count STARTING and STOPPING as 'running'
|
|
*/
|
|
isRunning: function() {
|
|
return isRunning;
|
|
}
|
|
});
|
|
|
|
exports.CSSUsageFront = CSSUsageFront;
|
|
|
|
/**
|
|
* Registration / De-registration
|
|
*/
|
|
exports.register = function(handle) {
|
|
handle.addGlobalActor(CSSUsageActor, "cssUsageActor");
|
|
handle.addTabActor(CSSUsageActor, "cssUsageActor");
|
|
};
|
|
|
|
exports.unregister = function(handle) {
|
|
handle.removeGlobalActor(CSSUsageActor, "cssUsageActor");
|
|
handle.removeTabActor(CSSUsageActor, "cssUsageActor");
|
|
};
|
|
|
|
const knownFronts = new WeakMap();
|
|
|
|
/**
|
|
* Create a CSSUsageFront only when needed (returns a promise)
|
|
* For notes on target.makeRemote(), see
|
|
* https://bugzilla.mozilla.org/show_bug.cgi?id=1016330#c7
|
|
*/
|
|
const getUsage = exports.getUsage = function(target) {
|
|
return target.makeRemote().then(() => {
|
|
let front = knownFronts.get(target.client)
|
|
if (front == null && target.form.cssUsageActor != null) {
|
|
front = new CSSUsageFront(target.client, target.form);
|
|
knownFronts.set(target.client, front);
|
|
}
|
|
return front;
|
|
});
|
|
};
|