csscoverage.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. const { Cc, Ci } = require("chrome");
  6. const Services = require("Services");
  7. const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
  8. const events = require("sdk/event/core");
  9. const protocol = require("devtools/shared/protocol");
  10. const { cssUsageSpec } = require("devtools/shared/specs/csscoverage");
  11. loader.lazyGetter(this, "DOMUtils", () => {
  12. return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
  13. });
  14. loader.lazyRequireGetter(this, "stylesheets", "devtools/server/actors/stylesheets");
  15. loader.lazyRequireGetter(this, "prettifyCSS", "devtools/shared/inspector/css-logic", true);
  16. const CSSRule = Ci.nsIDOMCSSRule;
  17. const MAX_UNUSED_RULES = 10000;
  18. /**
  19. * Allow: let foo = l10n.lookup("csscoverageFoo");
  20. */
  21. const l10n = exports.l10n = {
  22. _URI: "chrome://devtools-shared/locale/csscoverage.properties",
  23. lookup: function (msg) {
  24. if (this._stringBundle == null) {
  25. this._stringBundle = Services.strings.createBundle(this._URI);
  26. }
  27. return this._stringBundle.GetStringFromName(msg);
  28. }
  29. };
  30. /**
  31. * CSSUsage manages the collection of CSS usage data.
  32. * The core of a CSSUsage is a JSON-able data structure called _knownRules
  33. * which looks like this:
  34. * This records the CSSStyleRules and their usage.
  35. * The format is:
  36. * Map({
  37. * <CSS-URL>|<START-LINE>|<START-COLUMN>: {
  38. * selectorText: <CSSStyleRule.selectorText>,
  39. * test: <simplify(CSSStyleRule.selectorText)>,
  40. * cssText: <CSSStyleRule.cssText>,
  41. * isUsed: <TRUE|FALSE>,
  42. * presentOn: Set([ <HTML-URL>, ... ]),
  43. * preLoadOn: Set([ <HTML-URL>, ... ]),
  44. * isError: <TRUE|FALSE>,
  45. * }
  46. * })
  47. *
  48. * For example:
  49. * this._knownRules = Map({
  50. * "http://eg.com/styles1.css|15|0": {
  51. * selectorText: "p.quote:hover",
  52. * test: "p.quote",
  53. * cssText: "p.quote { color: red; }",
  54. * isUsed: true,
  55. * presentOn: Set([ "http://eg.com/page1.html", ... ]),
  56. * preLoadOn: Set([ "http://eg.com/page1.html" ]),
  57. * isError: false,
  58. * }, ...
  59. * });
  60. */
  61. var CSSUsageActor = protocol.ActorClassWithSpec(cssUsageSpec, {
  62. initialize: function (conn, tabActor) {
  63. protocol.Actor.prototype.initialize.call(this, conn);
  64. this._tabActor = tabActor;
  65. this._running = false;
  66. this._onTabLoad = this._onTabLoad.bind(this);
  67. this._onChange = this._onChange.bind(this);
  68. this._notifyOn = Ci.nsIWebProgress.NOTIFY_STATUS |
  69. Ci.nsIWebProgress.NOTIFY_STATE_ALL;
  70. },
  71. destroy: function () {
  72. this._tabActor = undefined;
  73. delete this._onTabLoad;
  74. delete this._onChange;
  75. protocol.Actor.prototype.destroy.call(this);
  76. },
  77. /**
  78. * Begin recording usage data
  79. * @param noreload It's best if we start by reloading the current page
  80. * because that starts the test at a known point, but there could be reasons
  81. * why we don't want to do that (e.g. the page contains state that will be
  82. * lost across a reload)
  83. */
  84. start: function (noreload) {
  85. if (this._running) {
  86. throw new Error(l10n.lookup("csscoverageRunningError"));
  87. }
  88. this._isOneShot = false;
  89. this._visitedPages = new Set();
  90. this._knownRules = new Map();
  91. this._running = true;
  92. this._tooManyUnused = false;
  93. this._progressListener = {
  94. QueryInterface: XPCOMUtils.generateQI([ Ci.nsIWebProgressListener,
  95. Ci.nsISupportsWeakReference ]),
  96. onStateChange: (progress, request, flags, status) => {
  97. let isStop = flags & Ci.nsIWebProgressListener.STATE_STOP;
  98. let isWindow = flags & Ci.nsIWebProgressListener.STATE_IS_WINDOW;
  99. if (isStop && isWindow) {
  100. this._onTabLoad(progress.DOMWindow.document);
  101. }
  102. },
  103. onLocationChange: () => {},
  104. onProgressChange: () => {},
  105. onSecurityChange: () => {},
  106. onStatusChange: () => {},
  107. destroy: () => {}
  108. };
  109. this._progress = this._tabActor.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
  110. .getInterface(Ci.nsIWebProgress);
  111. this._progress.addProgressListener(this._progressListener, this._notifyOn);
  112. if (noreload) {
  113. // If we're not starting by reloading the page, then pretend that onload
  114. // has just happened.
  115. this._onTabLoad(this._tabActor.window.document);
  116. } else {
  117. this._tabActor.window.location.reload();
  118. }
  119. events.emit(this, "state-change", { isRunning: true });
  120. },
  121. /**
  122. * Cease recording usage data
  123. */
  124. stop: function () {
  125. if (!this._running) {
  126. throw new Error(l10n.lookup("csscoverageNotRunningError"));
  127. }
  128. this._progress.removeProgressListener(this._progressListener, this._notifyOn);
  129. this._progress = undefined;
  130. this._running = false;
  131. events.emit(this, "state-change", { isRunning: false });
  132. },
  133. /**
  134. * Start/stop recording usage data depending on what we're currently doing.
  135. */
  136. toggle: function () {
  137. return this._running ? this.stop() : this.start();
  138. },
  139. /**
  140. * Running start() quickly followed by stop() does a bunch of unnecessary
  141. * work, so this cuts all that out
  142. */
  143. oneshot: function () {
  144. if (this._running) {
  145. throw new Error(l10n.lookup("csscoverageRunningError"));
  146. }
  147. this._isOneShot = true;
  148. this._visitedPages = new Set();
  149. this._knownRules = new Map();
  150. this._populateKnownRules(this._tabActor.window.document);
  151. this._updateUsage(this._tabActor.window.document, false);
  152. },
  153. /**
  154. * Called by the ProgressListener to simulate a "load" event
  155. */
  156. _onTabLoad: function (document) {
  157. this._populateKnownRules(document);
  158. this._updateUsage(document, true);
  159. this._observeMutations(document);
  160. },
  161. /**
  162. * Setup a MutationObserver on the current document
  163. */
  164. _observeMutations: function (document) {
  165. let MutationObserver = document.defaultView.MutationObserver;
  166. let observer = new MutationObserver(mutations => {
  167. // It's possible that one of the mutations in this list adds a 'use' of
  168. // a CSS rule, and another takes it away. See Bug 1010189
  169. this._onChange(document);
  170. });
  171. observer.observe(document, {
  172. attributes: true,
  173. childList: true,
  174. characterData: false,
  175. subtree: true
  176. });
  177. },
  178. /**
  179. * Event handler for whenever we think the page has changed in a way that
  180. * means the CSS usage might have changed.
  181. */
  182. _onChange: function (document) {
  183. // Ignore changes pre 'load'
  184. if (!this._visitedPages.has(getURL(document))) {
  185. return;
  186. }
  187. this._updateUsage(document, false);
  188. },
  189. /**
  190. * Called whenever we think the list of stylesheets might have changed so
  191. * we can update the list of rules that we should be checking
  192. */
  193. _populateKnownRules: function (document) {
  194. let url = getURL(document);
  195. this._visitedPages.add(url);
  196. // Go through all the rules in the current sheets adding them to knownRules
  197. // if needed and adding the current url to the list of pages they're on
  198. for (let rule of getAllSelectorRules(document)) {
  199. let ruleId = ruleToId(rule);
  200. let ruleData = this._knownRules.get(ruleId);
  201. if (ruleData == null) {
  202. ruleData = {
  203. selectorText: rule.selectorText,
  204. cssText: rule.cssText,
  205. test: getTestSelector(rule.selectorText),
  206. isUsed: false,
  207. presentOn: new Set(),
  208. preLoadOn: new Set(),
  209. isError: false
  210. };
  211. this._knownRules.set(ruleId, ruleData);
  212. }
  213. ruleData.presentOn.add(url);
  214. }
  215. },
  216. /**
  217. * Update knownRules with usage information from the current page
  218. */
  219. _updateUsage: function (document, isLoad) {
  220. let qsaCount = 0;
  221. // Update this._data with matches to say 'used at load time' by sheet X
  222. let url = getURL(document);
  223. for (let [ , ruleData ] of this._knownRules) {
  224. // If it broke before, don't try again selectors don't change
  225. if (ruleData.isError) {
  226. continue;
  227. }
  228. // If it's used somewhere already, don't bother checking again unless
  229. // this is a load event in which case we need to add preLoadOn
  230. if (!isLoad && ruleData.isUsed) {
  231. continue;
  232. }
  233. // Ignore rules that are not present on this page
  234. if (!ruleData.presentOn.has(url)) {
  235. continue;
  236. }
  237. qsaCount++;
  238. if (qsaCount > MAX_UNUSED_RULES) {
  239. console.error("Too many unused rules on " + url + " ");
  240. this._tooManyUnused = true;
  241. continue;
  242. }
  243. try {
  244. let match = document.querySelector(ruleData.test);
  245. if (match != null) {
  246. ruleData.isUsed = true;
  247. if (isLoad) {
  248. ruleData.preLoadOn.add(url);
  249. }
  250. }
  251. } catch (ex) {
  252. ruleData.isError = true;
  253. }
  254. }
  255. },
  256. /**
  257. * Returns a JSONable structure designed to help marking up the style editor,
  258. * which describes the CSS selector usage.
  259. * Example:
  260. * [
  261. * {
  262. * selectorText: "p#content",
  263. * usage: "unused|used",
  264. * start: { line: 3, column: 0 },
  265. * },
  266. * ...
  267. * ]
  268. */
  269. createEditorReport: function (url) {
  270. if (this._knownRules == null) {
  271. return { reports: [] };
  272. }
  273. let reports = [];
  274. for (let [ruleId, ruleData] of this._knownRules) {
  275. let { url: ruleUrl, line, column } = deconstructRuleId(ruleId);
  276. if (ruleUrl !== url || ruleData.isUsed) {
  277. continue;
  278. }
  279. let ruleReport = {
  280. selectorText: ruleData.selectorText,
  281. start: { line: line, column: column }
  282. };
  283. if (ruleData.end) {
  284. ruleReport.end = ruleData.end;
  285. }
  286. reports.push(ruleReport);
  287. }
  288. return { reports: reports };
  289. },
  290. /**
  291. * Compute the stylesheet URL and delegate the report creation to createEditorReport.
  292. * See createEditorReport documentation.
  293. *
  294. * @param {StyleSheetActor} stylesheetActor
  295. * the stylesheet actor for which the coverage report should be generated.
  296. */
  297. createEditorReportForSheet: function (stylesheetActor) {
  298. let url = sheetToUrl(stylesheetActor.rawSheet);
  299. return this.createEditorReport(url);
  300. },
  301. /**
  302. * Returns a JSONable structure designed for the page report which shows
  303. * the recommended changes to a page.
  304. *
  305. * "preload" means that a rule is used before the load event happens, which
  306. * means that the page could by optimized by placing it in a <style> element
  307. * at the top of the page, moving the <link> elements to the bottom.
  308. *
  309. * Example:
  310. * {
  311. * preload: [
  312. * {
  313. * url: "http://example.org/page1.html",
  314. * shortUrl: "page1.html",
  315. * rules: [
  316. * {
  317. * url: "http://example.org/style1.css",
  318. * shortUrl: "style1.css",
  319. * start: { line: 3, column: 4 },
  320. * selectorText: "p#content",
  321. * formattedCssText: "p#content {\n color: red;\n }\n"
  322. * },
  323. * ...
  324. * ]
  325. * }
  326. * ],
  327. * unused: [
  328. * {
  329. * url: "http://example.org/style1.css",
  330. * shortUrl: "style1.css",
  331. * rules: [ ... ]
  332. * }
  333. * ]
  334. * }
  335. */
  336. createPageReport: function () {
  337. if (this._running) {
  338. throw new Error(l10n.lookup("csscoverageRunningError"));
  339. }
  340. if (this._visitedPages == null) {
  341. throw new Error(l10n.lookup("csscoverageNotRunError"));
  342. }
  343. if (this._isOneShot) {
  344. throw new Error(l10n.lookup("csscoverageOneShotReportError"));
  345. }
  346. // Helper function to create a JSONable data structure representing a rule
  347. const ruleToRuleReport = function (rule, ruleData) {
  348. return {
  349. url: rule.url,
  350. shortUrl: rule.url.split("/").slice(-1)[0],
  351. start: { line: rule.line, column: rule.column },
  352. selectorText: ruleData.selectorText,
  353. formattedCssText: prettifyCSS(ruleData.cssText)
  354. };
  355. };
  356. // A count of each type of rule for the bar chart
  357. let summary = { used: 0, unused: 0, preload: 0 };
  358. // Create the set of the unused rules
  359. let unusedMap = new Map();
  360. for (let [ruleId, ruleData] of this._knownRules) {
  361. let rule = deconstructRuleId(ruleId);
  362. let rules = unusedMap.get(rule.url);
  363. if (rules == null) {
  364. rules = [];
  365. unusedMap.set(rule.url, rules);
  366. }
  367. if (!ruleData.isUsed) {
  368. let ruleReport = ruleToRuleReport(rule, ruleData);
  369. rules.push(ruleReport);
  370. } else {
  371. summary.unused++;
  372. }
  373. }
  374. let unused = [];
  375. for (let [url, rules] of unusedMap) {
  376. unused.push({
  377. url: url,
  378. shortUrl: url.split("/").slice(-1),
  379. rules: rules
  380. });
  381. }
  382. // Create the set of rules that could be pre-loaded
  383. let preload = [];
  384. for (let url of this._visitedPages) {
  385. let page = {
  386. url: url,
  387. shortUrl: url.split("/").slice(-1),
  388. rules: []
  389. };
  390. for (let [ruleId, ruleData] of this._knownRules) {
  391. if (ruleData.preLoadOn.has(url)) {
  392. let rule = deconstructRuleId(ruleId);
  393. let ruleReport = ruleToRuleReport(rule, ruleData);
  394. page.rules.push(ruleReport);
  395. summary.preload++;
  396. } else {
  397. summary.used++;
  398. }
  399. }
  400. if (page.rules.length > 0) {
  401. preload.push(page);
  402. }
  403. }
  404. return {
  405. summary: summary,
  406. preload: preload,
  407. unused: unused
  408. };
  409. },
  410. /**
  411. * For testing only. What pages did we visit.
  412. */
  413. _testOnlyVisitedPages: function () {
  414. return [...this._visitedPages];
  415. },
  416. });
  417. exports.CSSUsageActor = CSSUsageActor;
  418. /**
  419. * Generator that filters the CSSRules out of _getAllRules so it only
  420. * iterates over the CSSStyleRules
  421. */
  422. function* getAllSelectorRules(document) {
  423. for (let rule of getAllRules(document)) {
  424. if (rule.type === CSSRule.STYLE_RULE && rule.selectorText !== "") {
  425. yield rule;
  426. }
  427. }
  428. }
  429. /**
  430. * Generator to iterate over the CSSRules in all the stylesheets the
  431. * current document (i.e. it includes import rules, media rules, etc)
  432. */
  433. function* getAllRules(document) {
  434. // sheets is an array of the <link> and <style> element in this document
  435. let sheets = getAllSheets(document);
  436. for (let i = 0; i < sheets.length; i++) {
  437. for (let j = 0; j < sheets[i].cssRules.length; j++) {
  438. yield sheets[i].cssRules[j];
  439. }
  440. }
  441. }
  442. /**
  443. * Get an array of all the stylesheets that affect this document. That means
  444. * the <link> and <style> based sheets, and the @imported sheets (recursively)
  445. * but not the sheets in nested frames.
  446. */
  447. function getAllSheets(document) {
  448. // sheets is an array of the <link> and <style> element in this document
  449. let sheets = Array.slice(document.styleSheets);
  450. // Add @imported sheets
  451. for (let i = 0; i < sheets.length; i++) {
  452. let subSheets = getImportedSheets(sheets[i]);
  453. sheets = sheets.concat(...subSheets);
  454. }
  455. return sheets;
  456. }
  457. /**
  458. * Recursively find @import rules in the given stylesheet.
  459. * We're relying on the browser giving rule.styleSheet == null to resolve
  460. * @import loops
  461. */
  462. function getImportedSheets(stylesheet) {
  463. let sheets = [];
  464. for (let i = 0; i < stylesheet.cssRules.length; i++) {
  465. let rule = stylesheet.cssRules[i];
  466. // rule.styleSheet == null with duplicate @imports for the same URL.
  467. if (rule.type === CSSRule.IMPORT_RULE && rule.styleSheet != null) {
  468. sheets.push(rule.styleSheet);
  469. let subSheets = getImportedSheets(rule.styleSheet);
  470. sheets = sheets.concat(...subSheets);
  471. }
  472. }
  473. return sheets;
  474. }
  475. /**
  476. * Get a unique identifier for a rule. This is currently the string
  477. * <CSS-URL>|<START-LINE>|<START-COLUMN>
  478. * @see deconstructRuleId(ruleId)
  479. */
  480. function ruleToId(rule) {
  481. let line = DOMUtils.getRelativeRuleLine(rule);
  482. let column = DOMUtils.getRuleColumn(rule);
  483. return sheetToUrl(rule.parentStyleSheet) + "|" + line + "|" + column;
  484. }
  485. /**
  486. * Convert a ruleId to an object with { url, line, column } properties
  487. * @see ruleToId(rule)
  488. */
  489. const deconstructRuleId = exports.deconstructRuleId = function (ruleId) {
  490. let split = ruleId.split("|");
  491. if (split.length > 3) {
  492. let replace = split.slice(0, split.length - 3 + 1).join("|");
  493. split.splice(0, split.length - 3 + 1, replace);
  494. }
  495. let [ url, line, column ] = split;
  496. return {
  497. url: url,
  498. line: parseInt(line, 10),
  499. column: parseInt(column, 10)
  500. };
  501. };
  502. /**
  503. * We're only interested in the origin and pathname, because changes to the
  504. * username, password, hash, or query string probably don't significantly
  505. * change the CSS usage properties of a page.
  506. * @param document
  507. */
  508. const getURL = exports.getURL = function (document) {
  509. let url = new document.defaultView.URL(document.documentURI);
  510. return url == "about:blank" ? "" : "" + url.origin + url.pathname;
  511. };
  512. /**
  513. * Pseudo class handling constants:
  514. * We split pseudo-classes into a number of categories so we can decide how we
  515. * should match them. See getTestSelector for how we use these constants.
  516. *
  517. * @see http://dev.w3.org/csswg/selectors4/#overview
  518. * @see https://developer.mozilla.org/en-US/docs/tag/CSS%20Pseudo-class
  519. * @see https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements
  520. */
  521. /**
  522. * Category 1: Pseudo-classes that depend on external browser/OS state
  523. * This includes things like the time, locale, position of mouse/caret/window,
  524. * contents of browser history, etc. These can be hard to mimic.
  525. * Action: Remove from selectors
  526. */
  527. const SEL_EXTERNAL = [
  528. "active", "active-drop", "current", "dir", "focus", "future", "hover",
  529. "invalid-drop", "lang", "past", "placeholder-shown", "target", "valid-drop",
  530. "visited"
  531. ];
  532. /**
  533. * Category 2: Pseudo-classes that depend on user-input state
  534. * These are pseudo-classes that arguably *should* be covered by unit tests but
  535. * which probably aren't and which are unlikely to be covered by manual tests.
  536. * We're currently stripping them out,
  537. * Action: Remove from selectors (but consider future command line flag to
  538. * enable them in the future. e.g. 'csscoverage start --strict')
  539. */
  540. const SEL_FORM = [
  541. "checked", "default", "disabled", "enabled", "fullscreen", "in-range",
  542. "indeterminate", "invalid", "optional", "out-of-range", "required", "valid"
  543. ];
  544. /**
  545. * Category 3: Pseudo-elements
  546. * querySelectorAll doesn't return matches with pseudo-elements because there
  547. * is no element to match (they're pseudo) so we have to remove them all.
  548. * (See http://codepen.io/joewalker/pen/sanDw for a demo)
  549. * Action: Remove from selectors (including deprecated single colon versions)
  550. */
  551. const SEL_ELEMENT = [
  552. "after", "before", "first-letter", "first-line", "selection"
  553. ];
  554. /**
  555. * Category 4: Structural pseudo-classes
  556. * This is a category defined by the spec (also called tree-structural and
  557. * grid-structural) for selection based on relative position in the document
  558. * tree that cannot be represented by other simple selectors or combinators.
  559. * Action: Require a page-match
  560. */
  561. const SEL_STRUCTURAL = [
  562. "empty", "first-child", "first-of-type", "last-child", "last-of-type",
  563. "nth-column", "nth-last-column", "nth-child", "nth-last-child",
  564. "nth-last-of-type", "nth-of-type", "only-child", "only-of-type", "root"
  565. ];
  566. /**
  567. * Category 4a: Semi-structural pseudo-classes
  568. * These are not structural according to the spec, but act nevertheless on
  569. * information in the document tree.
  570. * Action: Require a page-match
  571. */
  572. const SEL_SEMI = [ "any-link", "link", "read-only", "read-write", "scope" ];
  573. /**
  574. * Category 5: Combining pseudo-classes
  575. * has(), not() etc join selectors together in various ways. We take care when
  576. * removing pseudo-classes to convert "not(:hover)" into "not(*)" and so on.
  577. * With these changes the combining pseudo-classes should probably stand on
  578. * their own.
  579. * Action: Require a page-match
  580. */
  581. const SEL_COMBINING = [ "not", "has", "matches" ];
  582. /**
  583. * Category 6: Media pseudo-classes
  584. * Pseudo-classes that should be ignored because they're only relevant to
  585. * media queries
  586. * Action: Don't need removing from selectors as they appear in media queries
  587. */
  588. const SEL_MEDIA = [ "blank", "first", "left", "right" ];
  589. /**
  590. * A test selector is a reduced form of a selector that we actually test
  591. * against. This code strips out pseudo-elements and some pseudo-classes that
  592. * we think should not have to match in order for the selector to be relevant.
  593. */
  594. function getTestSelector(selector) {
  595. let replacement = selector;
  596. let replaceSelector = pseudo => {
  597. replacement = replacement.replace(" :" + pseudo, " *")
  598. .replace("(:" + pseudo, "(*")
  599. .replace(":" + pseudo, "");
  600. };
  601. SEL_EXTERNAL.forEach(replaceSelector);
  602. SEL_FORM.forEach(replaceSelector);
  603. SEL_ELEMENT.forEach(replaceSelector);
  604. // Pseudo elements work in : and :: forms
  605. SEL_ELEMENT.forEach(pseudo => {
  606. replacement = replacement.replace("::" + pseudo, "");
  607. });
  608. return replacement;
  609. }
  610. /**
  611. * I've documented all known pseudo-classes above for 2 reasons: To allow
  612. * checking logic and what might be missing, but also to allow a unit test
  613. * that fetches the list of supported pseudo-classes and pseudo-elements from
  614. * the platform and check that they were all represented here.
  615. */
  616. exports.SEL_ALL = [
  617. SEL_EXTERNAL, SEL_FORM, SEL_ELEMENT, SEL_STRUCTURAL, SEL_SEMI,
  618. SEL_COMBINING, SEL_MEDIA
  619. ].reduce(function (prev, curr) {
  620. return prev.concat(curr);
  621. }, []);
  622. /**
  623. * Find a URL for a given stylesheet
  624. * @param {StyleSheet} stylesheet raw stylesheet
  625. */
  626. const sheetToUrl = function (stylesheet) {
  627. // For <link> elements
  628. if (stylesheet.href) {
  629. return stylesheet.href;
  630. }
  631. // For <style> elements
  632. if (stylesheet.ownerNode) {
  633. let document = stylesheet.ownerNode.ownerDocument;
  634. let sheets = [...document.querySelectorAll("style")];
  635. let index = sheets.indexOf(stylesheet.ownerNode);
  636. return getURL(document) + " → <style> index " + index;
  637. }
  638. throw new Error("Unknown sheet source");
  639. };