зеркало из https://github.com/mozilla/gecko-dev.git
716 строки
21 KiB
JavaScript
716 строки
21 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 { Ci } = require("chrome");
|
|
|
|
const InspectorUtils = require("InspectorUtils");
|
|
const Services = require("Services");
|
|
const ChromeUtils = require("ChromeUtils");
|
|
|
|
const protocol = require("devtools/shared/protocol");
|
|
const { cssUsageSpec } = require("devtools/shared/specs/csscoverage");
|
|
|
|
loader.lazyRequireGetter(this, "prettifyCSS", "devtools/shared/inspector/css-logic", true);
|
|
|
|
const MAX_UNUSED_RULES = 10000;
|
|
|
|
/**
|
|
* Allow: let foo = l10n.lookup("csscoverageFoo");
|
|
*/
|
|
const l10n = exports.l10n = {
|
|
_URI: "chrome://devtools-shared/locale/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,
|
|
* }, ...
|
|
* });
|
|
*/
|
|
var CSSUsageActor = protocol.ActorClassWithSpec(cssUsageSpec, {
|
|
initialize: function(conn, targetActor) {
|
|
protocol.Actor.prototype.initialize.call(this, conn);
|
|
|
|
this._targetActor = targetActor;
|
|
this._running = false;
|
|
|
|
this._onTabLoad = this._onTabLoad.bind(this);
|
|
this._onChange = this._onChange.bind(this);
|
|
|
|
this._notifyOn = Ci.nsIWebProgress.NOTIFY_STATE_ALL;
|
|
},
|
|
|
|
destroy: function() {
|
|
this._targetActor = 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: 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: ChromeUtils.generateQI([ Ci.nsIWebProgressListener,
|
|
Ci.nsISupportsWeakReference ]),
|
|
|
|
onStateChange: (progress, request, flags, status) => {
|
|
const isStop = flags & Ci.nsIWebProgressListener.STATE_STOP;
|
|
const isWindow = flags & Ci.nsIWebProgressListener.STATE_IS_WINDOW;
|
|
|
|
if (isStop && isWindow) {
|
|
this._onTabLoad(progress.DOMWindow.document);
|
|
}
|
|
},
|
|
|
|
destroy: () => {}
|
|
};
|
|
|
|
this._progress = this._targetActor.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._targetActor.window.document);
|
|
} else {
|
|
this._targetActor.window.location.reload();
|
|
}
|
|
|
|
this.emit("state-change", { isRunning: true });
|
|
},
|
|
|
|
/**
|
|
* Cease recording usage data
|
|
*/
|
|
stop: function() {
|
|
if (!this._running) {
|
|
throw new Error(l10n.lookup("csscoverageNotRunningError"));
|
|
}
|
|
|
|
this._progress.removeProgressListener(this._progressListener, this._notifyOn);
|
|
this._progress = undefined;
|
|
|
|
this._running = false;
|
|
this.emit("state-change", { isRunning: false });
|
|
},
|
|
|
|
/**
|
|
* Start/stop recording usage data depending on what we're currently doing.
|
|
*/
|
|
toggle: 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: function() {
|
|
if (this._running) {
|
|
throw new Error(l10n.lookup("csscoverageRunningError"));
|
|
}
|
|
|
|
this._isOneShot = true;
|
|
this._visitedPages = new Set();
|
|
this._knownRules = new Map();
|
|
|
|
this._populateKnownRules(this._targetActor.window.document);
|
|
this._updateUsage(this._targetActor.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) {
|
|
const MutationObserver = document.defaultView.MutationObserver;
|
|
const 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) {
|
|
const 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 (const rule of getAllSelectorRules(document)) {
|
|
const 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
|
|
const url = getURL(document);
|
|
|
|
for (const [ , 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 {
|
|
const 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: function(url) {
|
|
if (this._knownRules == null) {
|
|
return { reports: [] };
|
|
}
|
|
|
|
const reports = [];
|
|
for (const [ruleId, ruleData] of this._knownRules) {
|
|
const { url: ruleUrl, line, column } = deconstructRuleId(ruleId);
|
|
if (ruleUrl !== url || ruleData.isUsed) {
|
|
continue;
|
|
}
|
|
|
|
const ruleReport = {
|
|
selectorText: ruleData.selectorText,
|
|
start: { line: line, column: column }
|
|
};
|
|
|
|
if (ruleData.end) {
|
|
ruleReport.end = ruleData.end;
|
|
}
|
|
|
|
reports.push(ruleReport);
|
|
}
|
|
|
|
return { reports: reports };
|
|
},
|
|
|
|
/**
|
|
* Compute the stylesheet URL and delegate the report creation to createEditorReport.
|
|
* See createEditorReport documentation.
|
|
*
|
|
* @param {StyleSheetActor} stylesheetActor
|
|
* the stylesheet actor for which the coverage report should be generated.
|
|
*/
|
|
createEditorReportForSheet: function(stylesheetActor) {
|
|
const url = sheetToUrl(stylesheetActor.rawSheet);
|
|
return this.createEditorReport(url);
|
|
},
|
|
|
|
/**
|
|
* 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: 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: prettifyCSS(ruleData.cssText)
|
|
};
|
|
};
|
|
|
|
// A count of each type of rule for the bar chart
|
|
const summary = { used: 0, unused: 0, preload: 0 };
|
|
|
|
// Create the set of the unused rules
|
|
const unusedMap = new Map();
|
|
for (const [ruleId, ruleData] of this._knownRules) {
|
|
const rule = deconstructRuleId(ruleId);
|
|
let rules = unusedMap.get(rule.url);
|
|
if (rules == null) {
|
|
rules = [];
|
|
unusedMap.set(rule.url, rules);
|
|
}
|
|
if (!ruleData.isUsed) {
|
|
const ruleReport = ruleToRuleReport(rule, ruleData);
|
|
rules.push(ruleReport);
|
|
} else {
|
|
summary.unused++;
|
|
}
|
|
}
|
|
const unused = [];
|
|
for (const [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
|
|
const preload = [];
|
|
for (const url of this._visitedPages) {
|
|
const page = {
|
|
url: url,
|
|
shortUrl: url.split("/").slice(-1),
|
|
rules: []
|
|
};
|
|
|
|
for (const [ruleId, ruleData] of this._knownRules) {
|
|
if (ruleData.preLoadOn.has(url)) {
|
|
const rule = deconstructRuleId(ruleId);
|
|
const 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
|
|
};
|
|
},
|
|
|
|
/**
|
|
* For testing only. What pages did we visit.
|
|
*/
|
|
_testOnlyVisitedPages: function() {
|
|
return [...this._visitedPages];
|
|
},
|
|
});
|
|
|
|
exports.CSSUsageActor = CSSUsageActor;
|
|
|
|
/**
|
|
* Generator that filters the CSSRules out of _getAllRules so it only
|
|
* iterates over the CSSStyleRules
|
|
*/
|
|
function* getAllSelectorRules(document) {
|
|
for (const 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
|
|
const 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++) {
|
|
const 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++) {
|
|
const 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);
|
|
const 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) {
|
|
const line = InspectorUtils.getRelativeRuleLine(rule);
|
|
const column = InspectorUtils.getRuleColumn(rule);
|
|
return sheetToUrl(rule.parentStyleSheet) + "|" + line + "|" + column;
|
|
}
|
|
|
|
/**
|
|
* Convert a ruleId to an object with { url, line, column } properties
|
|
* @see ruleToId(rule)
|
|
*/
|
|
const deconstructRuleId = exports.deconstructRuleId = function(ruleId) {
|
|
const split = ruleId.split("|");
|
|
if (split.length > 3) {
|
|
const replace = split.slice(0, split.length - 3 + 1).join("|");
|
|
split.splice(0, split.length - 3 + 1, replace);
|
|
}
|
|
const [ 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) {
|
|
const url = new document.defaultView.URL(document.documentURI);
|
|
return url == "about:blank" ? "" : "" + 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;
|
|
const 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
|
|
* @param {StyleSheet} stylesheet raw stylesheet
|
|
*/
|
|
const sheetToUrl = function(stylesheet) {
|
|
// For <link> elements
|
|
if (stylesheet.href) {
|
|
return stylesheet.href;
|
|
}
|
|
|
|
// For <style> elements
|
|
if (stylesheet.ownerNode) {
|
|
const document = stylesheet.ownerNode.ownerDocument;
|
|
const sheets = [...document.querySelectorAll("style")];
|
|
const index = sheets.indexOf(stylesheet.ownerNode);
|
|
return getURL(document) + " → <style> index " + index;
|
|
}
|
|
|
|
throw new Error("Unknown sheet source");
|
|
};
|