css-logic.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
  2. /* This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  5. /*
  6. * About the objects defined in this file:
  7. * - CssLogic contains style information about a view context. It provides
  8. * access to 2 sets of objects: Css[Sheet|Rule|Selector] provide access to
  9. * information that does not change when the selected element changes while
  10. * Css[Property|Selector]Info provide information that is dependent on the
  11. * selected element.
  12. * Its key methods are highlight(), getPropertyInfo() and forEachSheet(), etc
  13. * It also contains a number of static methods for l10n, naming, etc
  14. *
  15. * - CssSheet provides a more useful API to a DOM CSSSheet for our purposes,
  16. * including shortSource and href.
  17. * - CssRule a more useful API to a nsIDOMCSSRule including access to the group
  18. * of CssSelectors that the rule provides properties for
  19. * - CssSelector A single selector - i.e. not a selector group. In other words
  20. * a CssSelector does not contain ','. This terminology is different from the
  21. * standard DOM API, but more inline with the definition in the spec.
  22. *
  23. * - CssPropertyInfo contains style information for a single property for the
  24. * highlighted element.
  25. * - CssSelectorInfo is a wrapper around CssSelector, which adds sorting with
  26. * reference to the selected element.
  27. */
  28. "use strict";
  29. const MAX_DATA_URL_LENGTH = 40;
  30. /**
  31. * Provide access to the style information in a page.
  32. * CssLogic uses the standard DOM API, and the Gecko inIDOMUtils API to access
  33. * styling information in the page, and present this to the user in a way that
  34. * helps them understand:
  35. * - why their expectations may not have been fulfilled
  36. * - how browsers process CSS
  37. * @constructor
  38. */
  39. const Services = require("Services");
  40. const CSSLexer = require("devtools/shared/css/lexer");
  41. const {LocalizationHelper} = require("devtools/shared/l10n");
  42. const styleInspectorL10N =
  43. new LocalizationHelper("devtools/shared/locales/styleinspector.properties");
  44. /**
  45. * Special values for filter, in addition to an href these values can be used
  46. */
  47. exports.FILTER = {
  48. // show properties for all user style sheets.
  49. USER: "user",
  50. // USER, plus user-agent (i.e. browser) style sheets
  51. UA: "ua",
  52. };
  53. /**
  54. * Each rule has a status, the bigger the number, the better placed it is to
  55. * provide styling information.
  56. *
  57. * These statuses are localized inside the styleinspector.properties
  58. * string bundle.
  59. * @see csshtmltree.js RuleView._cacheStatusNames()
  60. */
  61. exports.STATUS = {
  62. BEST: 3,
  63. MATCHED: 2,
  64. PARENT_MATCH: 1,
  65. UNMATCHED: 0,
  66. UNKNOWN: -1,
  67. };
  68. /**
  69. * Lookup a l10n string in the shared styleinspector string bundle.
  70. *
  71. * @param {String} name
  72. * The key to lookup.
  73. * @returns {String} A localized version of the given key.
  74. */
  75. exports.l10n = name => styleInspectorL10N.getStr(name);
  76. /**
  77. * Is the given property sheet a content stylesheet?
  78. *
  79. * @param {CSSStyleSheet} sheet a stylesheet
  80. * @return {boolean} true if the given stylesheet is a content stylesheet,
  81. * false otherwise.
  82. */
  83. exports.isContentStylesheet = function (sheet) {
  84. return sheet.parsingMode !== "agent";
  85. };
  86. /**
  87. * Return a shortened version of a style sheet's source.
  88. *
  89. * @param {CSSStyleSheet} sheet the DOM object for the style sheet.
  90. */
  91. exports.shortSource = function (sheet) {
  92. // Use a string like "inline" if there is no source href
  93. if (!sheet || !sheet.href) {
  94. return exports.l10n("rule.sourceInline");
  95. }
  96. // If the sheet is a data URL, return a trimmed version of it.
  97. let dataUrl = sheet.href.trim().match(/^data:.*?,((?:.|\r|\n)*)$/);
  98. if (dataUrl) {
  99. return dataUrl[1].length > MAX_DATA_URL_LENGTH ?
  100. `${dataUrl[1].substr(0, MAX_DATA_URL_LENGTH - 1)}…` : dataUrl[1];
  101. }
  102. // We try, in turn, the filename, filePath, query string, whole thing
  103. let url = {};
  104. try {
  105. url = new URL(sheet.href);
  106. } catch (ex) {
  107. // Some UA-provided stylesheets are not valid URLs.
  108. }
  109. if (url.pathname) {
  110. let index = url.pathname.lastIndexOf("/");
  111. if (index !== -1 && index < url.pathname.length) {
  112. return url.pathname.slice(index + 1);
  113. }
  114. return url.pathname;
  115. }
  116. if (url.query) {
  117. return url.query;
  118. }
  119. return sheet.href;
  120. };
  121. const TAB_CHARS = "\t";
  122. /**
  123. * Prettify minified CSS text.
  124. * This prettifies CSS code where there is no indentation in usual places while
  125. * keeping original indentation as-is elsewhere.
  126. * @param string text The CSS source to prettify.
  127. * @return string Prettified CSS source
  128. */
  129. function prettifyCSS(text, ruleCount) {
  130. if (prettifyCSS.LINE_SEPARATOR == null) {
  131. let os = Services.appinfo.OS;
  132. prettifyCSS.LINE_SEPARATOR = (os === "WINNT" ? "\r\n" : "\n");
  133. }
  134. // remove initial and terminating HTML comments and surrounding whitespace
  135. text = text.replace(/(?:^\s*<!--[\r\n]*)|(?:\s*-->\s*$)/g, "");
  136. let originalText = text;
  137. text = text.trim();
  138. // don't attempt to prettify if there's more than one line per rule.
  139. let lineCount = text.split("\n").length - 1;
  140. if (ruleCount !== null && lineCount >= ruleCount) {
  141. return originalText;
  142. }
  143. // We reformat the text using a simple state machine. The
  144. // reformatting preserves most of the input text, changing only
  145. // whitespace. The rules are:
  146. //
  147. // * After a "{" or ";" symbol, ensure there is a newline and
  148. // indentation before the next non-comment, non-whitespace token.
  149. // * Additionally after a "{" symbol, increase the indentation.
  150. // * A "}" symbol ensures there is a preceding newline, and
  151. // decreases the indentation level.
  152. // * Ensure there is whitespace before a "{".
  153. //
  154. // This approach can be confused sometimes, but should do ok on a
  155. // minified file.
  156. let indent = "";
  157. let indentLevel = 0;
  158. let tokens = CSSLexer.getCSSLexer(text);
  159. let result = "";
  160. let pushbackToken = undefined;
  161. // A helper function that reads tokens, looking for the next
  162. // non-comment, non-whitespace token. Comment and whitespace tokens
  163. // are appended to |result|. If this encounters EOF, it returns
  164. // null. Otherwise it returns the last whitespace token that was
  165. // seen. This function also updates |pushbackToken|.
  166. let readUntilSignificantToken = () => {
  167. while (true) {
  168. let token = tokens.nextToken();
  169. if (!token || token.tokenType !== "whitespace") {
  170. pushbackToken = token;
  171. return token;
  172. }
  173. // Saw whitespace. Before committing to it, check the next
  174. // token.
  175. let nextToken = tokens.nextToken();
  176. if (!nextToken || nextToken.tokenType !== "comment") {
  177. pushbackToken = nextToken;
  178. return token;
  179. }
  180. // Saw whitespace + comment. Update the result and continue.
  181. result = result + text.substring(token.startOffset, nextToken.endOffset);
  182. }
  183. };
  184. // State variables for readUntilNewlineNeeded.
  185. //
  186. // Starting index of the accumulated tokens.
  187. let startIndex;
  188. // Ending index of the accumulated tokens.
  189. let endIndex;
  190. // True if any non-whitespace token was seen.
  191. let anyNonWS;
  192. // True if the terminating token is "}".
  193. let isCloseBrace;
  194. // True if the token just before the terminating token was
  195. // whitespace.
  196. let lastWasWS;
  197. // A helper function that reads tokens until there is a reason to
  198. // insert a newline. This updates the state variables as needed.
  199. // If this encounters EOF, it returns null. Otherwise it returns
  200. // the final token read. Note that if the returned token is "{",
  201. // then it will not be included in the computed start/end token
  202. // range. This is used to handle whitespace insertion before a "{".
  203. let readUntilNewlineNeeded = () => {
  204. let token;
  205. while (true) {
  206. if (pushbackToken) {
  207. token = pushbackToken;
  208. pushbackToken = undefined;
  209. } else {
  210. token = tokens.nextToken();
  211. }
  212. if (!token) {
  213. endIndex = text.length;
  214. break;
  215. }
  216. // A "}" symbol must be inserted later, to deal with indentation
  217. // and newline.
  218. if (token.tokenType === "symbol" && token.text === "}") {
  219. isCloseBrace = true;
  220. break;
  221. } else if (token.tokenType === "symbol" && token.text === "{") {
  222. break;
  223. }
  224. if (token.tokenType !== "whitespace") {
  225. anyNonWS = true;
  226. }
  227. if (startIndex === undefined) {
  228. startIndex = token.startOffset;
  229. }
  230. endIndex = token.endOffset;
  231. if (token.tokenType === "symbol" && token.text === ";") {
  232. break;
  233. }
  234. lastWasWS = token.tokenType === "whitespace";
  235. }
  236. return token;
  237. };
  238. while (true) {
  239. // Set the initial state.
  240. startIndex = undefined;
  241. endIndex = undefined;
  242. anyNonWS = false;
  243. isCloseBrace = false;
  244. lastWasWS = false;
  245. // Read tokens until we see a reason to insert a newline.
  246. let token = readUntilNewlineNeeded();
  247. // Append any saved up text to the result, applying indentation.
  248. if (startIndex !== undefined) {
  249. if (isCloseBrace && !anyNonWS) {
  250. // If we saw only whitespace followed by a "}", then we don't
  251. // need anything here.
  252. } else {
  253. result = result + indent + text.substring(startIndex, endIndex);
  254. if (isCloseBrace) {
  255. result += prettifyCSS.LINE_SEPARATOR;
  256. }
  257. }
  258. }
  259. if (isCloseBrace) {
  260. indent = TAB_CHARS.repeat(--indentLevel);
  261. result = result + indent + "}";
  262. }
  263. if (!token) {
  264. break;
  265. }
  266. if (token.tokenType === "symbol" && token.text === "{") {
  267. if (!lastWasWS) {
  268. result += " ";
  269. }
  270. result += "{";
  271. indent = TAB_CHARS.repeat(++indentLevel);
  272. }
  273. // Now it is time to insert a newline. However first we want to
  274. // deal with any trailing comments.
  275. token = readUntilSignificantToken();
  276. // "Early" bail-out if the text does not appear to be minified.
  277. // Here we ignore the case where whitespace appears at the end of
  278. // the text.
  279. if (pushbackToken && token && token.tokenType === "whitespace" &&
  280. /\n/g.test(text.substring(token.startOffset, token.endOffset))) {
  281. return originalText;
  282. }
  283. // Finally time for that newline.
  284. result = result + prettifyCSS.LINE_SEPARATOR;
  285. // Maybe we hit EOF.
  286. if (!pushbackToken) {
  287. break;
  288. }
  289. }
  290. return result;
  291. }
  292. exports.prettifyCSS = prettifyCSS;