mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-05 07:18:39 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
726
devtools/server/actors/csscoverage.js
Normal file
726
devtools/server/actors/csscoverage.js
Normal file
|
|
@ -0,0 +1,726 @@
|
|||
/* 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 } = require("chrome");
|
||||
|
||||
const Services = require("Services");
|
||||
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
const events = require("sdk/event/core");
|
||||
const protocol = require("devtools/shared/protocol");
|
||||
const { cssUsageSpec } = require("devtools/shared/specs/csscoverage");
|
||||
|
||||
loader.lazyGetter(this, "DOMUtils", () => {
|
||||
return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
|
||||
});
|
||||
loader.lazyRequireGetter(this, "stylesheets", "devtools/server/actors/stylesheets");
|
||||
loader.lazyRequireGetter(this, "prettifyCSS", "devtools/shared/inspector/css-logic", true);
|
||||
|
||||
const CSSRule = Ci.nsIDOMCSSRule;
|
||||
|
||||
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, 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: 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 });
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
events.emit(this, "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._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: 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 };
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
let 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
|
||||
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
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 (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 line = DOMUtils.getRelativeRuleLine(rule);
|
||||
let column = DOMUtils.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) {
|
||||
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 == "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;
|
||||
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
|
||||
* @param {StyleSheet} stylesheet raw stylesheet
|
||||
*/
|
||||
const 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");
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue