utils.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  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. /* globals document, window */
  6. /* import-globals-from ./debugger-controller.js */
  7. "use strict";
  8. // Maps known URLs to friendly source group names and put them at the
  9. // bottom of source list.
  10. var KNOWN_SOURCE_GROUPS = {
  11. "Add-on SDK": "resource://gre/modules/commonjs/",
  12. };
  13. KNOWN_SOURCE_GROUPS[L10N.getStr("anonymousSourcesLabel")] = "anonymous";
  14. var XULUtils = {
  15. /**
  16. * Create <command> elements within `commandset` with event handlers
  17. * bound to the `command` event
  18. *
  19. * @param commandset HTML Element
  20. * A <commandset> element
  21. * @param commands Object
  22. * An object where keys specify <command> ids and values
  23. * specify event handlers to be bound on the `command` event
  24. */
  25. addCommands: function (commandset, commands) {
  26. Object.keys(commands).forEach(name => {
  27. let node = document.createElement("command");
  28. node.id = name;
  29. // XXX bug 371900: the command element must have an oncommand
  30. // attribute as a string set by `setAttribute` for keys to use it
  31. node.setAttribute("oncommand", " ");
  32. node.addEventListener("command", commands[name]);
  33. commandset.appendChild(node);
  34. });
  35. }
  36. };
  37. // Used to detect minification for automatic pretty printing
  38. const SAMPLE_SIZE = 50; // no of lines
  39. const INDENT_COUNT_THRESHOLD = 5; // percentage
  40. const CHARACTER_LIMIT = 250; // line character limit
  41. /**
  42. * Utility functions for handling sources.
  43. */
  44. var SourceUtils = {
  45. _labelsCache: new Map(), // Can't use WeakMaps because keys are strings.
  46. _groupsCache: new Map(),
  47. _minifiedCache: new Map(),
  48. /**
  49. * Returns true if the specified url and/or content type are specific to
  50. * javascript files.
  51. *
  52. * @return boolean
  53. * True if the source is likely javascript.
  54. */
  55. isJavaScript: function (aUrl, aContentType = "") {
  56. return (aUrl && /\.jsm?$/.test(this.trimUrlQuery(aUrl))) ||
  57. aContentType.includes("javascript");
  58. },
  59. /**
  60. * Determines if the source text is minified by using
  61. * the percentage indented of a subset of lines
  62. *
  63. * @return object
  64. * A promise that resolves to true if source text is minified.
  65. */
  66. isMinified: function (key, text) {
  67. if (this._minifiedCache.has(key)) {
  68. return this._minifiedCache.get(key);
  69. }
  70. let isMinified;
  71. let lineEndIndex = 0;
  72. let lineStartIndex = 0;
  73. let lines = 0;
  74. let indentCount = 0;
  75. let overCharLimit = false;
  76. // Strip comments.
  77. text = text.replace(/\/\*[\S\s]*?\*\/|\/\/(.+|\n)/g, "");
  78. while (lines++ < SAMPLE_SIZE) {
  79. lineEndIndex = text.indexOf("\n", lineStartIndex);
  80. if (lineEndIndex == -1) {
  81. break;
  82. }
  83. if (/^\s+/.test(text.slice(lineStartIndex, lineEndIndex))) {
  84. indentCount++;
  85. }
  86. // For files with no indents but are not minified.
  87. if ((lineEndIndex - lineStartIndex) > CHARACTER_LIMIT) {
  88. overCharLimit = true;
  89. break;
  90. }
  91. lineStartIndex = lineEndIndex + 1;
  92. }
  93. isMinified =
  94. ((indentCount / lines) * 100) < INDENT_COUNT_THRESHOLD || overCharLimit;
  95. this._minifiedCache.set(key, isMinified);
  96. return isMinified;
  97. },
  98. /**
  99. * Clears the labels, groups and minify cache, populated by methods like
  100. * SourceUtils.getSourceLabel or Source Utils.getSourceGroup.
  101. * This should be done every time the content location changes.
  102. */
  103. clearCache: function () {
  104. this._labelsCache.clear();
  105. this._groupsCache.clear();
  106. this._minifiedCache.clear();
  107. },
  108. /**
  109. * Gets a unique, simplified label from a source url.
  110. *
  111. * @param string aUrl
  112. * The source url.
  113. * @return string
  114. * The simplified label.
  115. */
  116. getSourceLabel: function (aUrl) {
  117. let cachedLabel = this._labelsCache.get(aUrl);
  118. if (cachedLabel) {
  119. return cachedLabel;
  120. }
  121. let sourceLabel = null;
  122. for (let name of Object.keys(KNOWN_SOURCE_GROUPS)) {
  123. if (aUrl.startsWith(KNOWN_SOURCE_GROUPS[name])) {
  124. sourceLabel = aUrl.substring(KNOWN_SOURCE_GROUPS[name].length);
  125. }
  126. }
  127. if (!sourceLabel) {
  128. sourceLabel = this.trimUrl(aUrl);
  129. }
  130. let unicodeLabel = NetworkHelper.convertToUnicode(unescape(sourceLabel));
  131. this._labelsCache.set(aUrl, unicodeLabel);
  132. return unicodeLabel;
  133. },
  134. /**
  135. * Gets as much information as possible about the hostname and directory paths
  136. * of an url to create a short url group identifier.
  137. *
  138. * @param string aUrl
  139. * The source url.
  140. * @return string
  141. * The simplified group.
  142. */
  143. getSourceGroup: function (aUrl) {
  144. let cachedGroup = this._groupsCache.get(aUrl);
  145. if (cachedGroup) {
  146. return cachedGroup;
  147. }
  148. try {
  149. // Use an nsIURL to parse all the url path parts.
  150. var uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
  151. } catch (e) {
  152. // This doesn't look like a url, or nsIURL can't handle it.
  153. return "";
  154. }
  155. let groupLabel = uri.prePath;
  156. for (let name of Object.keys(KNOWN_SOURCE_GROUPS)) {
  157. if (aUrl.startsWith(KNOWN_SOURCE_GROUPS[name])) {
  158. groupLabel = name;
  159. }
  160. }
  161. let unicodeLabel = NetworkHelper.convertToUnicode(unescape(groupLabel));
  162. this._groupsCache.set(aUrl, unicodeLabel);
  163. return unicodeLabel;
  164. },
  165. /**
  166. * Trims the url by shortening it if it exceeds a certain length, adding an
  167. * ellipsis at the end.
  168. *
  169. * @param string aUrl
  170. * The source url.
  171. * @param number aLength [optional]
  172. * The expected source url length.
  173. * @param number aSection [optional]
  174. * The section to trim. Supported values: "start", "center", "end"
  175. * @return string
  176. * The shortened url.
  177. */
  178. trimUrlLength: function (aUrl, aLength, aSection) {
  179. aLength = aLength || SOURCE_URL_DEFAULT_MAX_LENGTH;
  180. aSection = aSection || "end";
  181. if (aUrl.length > aLength) {
  182. switch (aSection) {
  183. case "start":
  184. return ELLIPSIS + aUrl.slice(-aLength);
  185. break;
  186. case "center":
  187. return aUrl.substr(0, aLength / 2 - 1) + ELLIPSIS + aUrl.slice(-aLength / 2 + 1);
  188. break;
  189. case "end":
  190. return aUrl.substr(0, aLength) + ELLIPSIS;
  191. break;
  192. }
  193. }
  194. return aUrl;
  195. },
  196. /**
  197. * Trims the query part or reference identifier of a url string, if necessary.
  198. *
  199. * @param string aUrl
  200. * The source url.
  201. * @return string
  202. * The shortened url.
  203. */
  204. trimUrlQuery: function (aUrl) {
  205. let length = aUrl.length;
  206. let q1 = aUrl.indexOf("?");
  207. let q2 = aUrl.indexOf("&");
  208. let q3 = aUrl.indexOf("#");
  209. let q = Math.min(q1 != -1 ? q1 : length,
  210. q2 != -1 ? q2 : length,
  211. q3 != -1 ? q3 : length);
  212. return aUrl.slice(0, q);
  213. },
  214. /**
  215. * Trims as much as possible from a url, while keeping the label unique
  216. * in the sources container.
  217. *
  218. * @param string | nsIURL aUrl
  219. * The source url.
  220. * @param string aLabel [optional]
  221. * The resulting label at each step.
  222. * @param number aSeq [optional]
  223. * The current iteration step.
  224. * @return string
  225. * The resulting label at the final step.
  226. */
  227. trimUrl: function (aUrl, aLabel, aSeq) {
  228. if (!(aUrl instanceof Ci.nsIURL)) {
  229. try {
  230. // Use an nsIURL to parse all the url path parts.
  231. aUrl = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
  232. } catch (e) {
  233. // This doesn't look like a url, or nsIURL can't handle it.
  234. return aUrl;
  235. }
  236. }
  237. if (!aSeq) {
  238. let name = aUrl.fileName;
  239. if (name) {
  240. // This is a regular file url, get only the file name (contains the
  241. // base name and extension if available).
  242. // If this url contains an invalid query, unfortunately nsIURL thinks
  243. // it's part of the file extension. It must be removed.
  244. aLabel = aUrl.fileName.replace(/\&.*/, "");
  245. } else {
  246. // This is not a file url, hence there is no base name, nor extension.
  247. // Proceed using other available information.
  248. aLabel = "";
  249. }
  250. aSeq = 1;
  251. }
  252. // If we have a label and it doesn't only contain a query...
  253. if (aLabel && aLabel.indexOf("?") != 0) {
  254. // A page may contain multiple requests to the same url but with different
  255. // queries. It is *not* redundant to show each one.
  256. if (!DebuggerView.Sources.getItemForAttachment(e => e.label == aLabel)) {
  257. return aLabel;
  258. }
  259. }
  260. // Append the url query.
  261. if (aSeq == 1) {
  262. let query = aUrl.query;
  263. if (query) {
  264. return this.trimUrl(aUrl, aLabel + "?" + query, aSeq + 1);
  265. }
  266. aSeq++;
  267. }
  268. // Append the url reference.
  269. if (aSeq == 2) {
  270. let ref = aUrl.ref;
  271. if (ref) {
  272. return this.trimUrl(aUrl, aLabel + "#" + aUrl.ref, aSeq + 1);
  273. }
  274. aSeq++;
  275. }
  276. // Prepend the url directory.
  277. if (aSeq == 3) {
  278. let dir = aUrl.directory;
  279. if (dir) {
  280. return this.trimUrl(aUrl, dir.replace(/^\//, "") + aLabel, aSeq + 1);
  281. }
  282. aSeq++;
  283. }
  284. // Prepend the hostname and port number.
  285. if (aSeq == 4) {
  286. let host;
  287. try {
  288. // Bug 1261860: jar: URLs throw when accessing `hostPost`
  289. host = aUrl.hostPort;
  290. } catch (e) {}
  291. if (host) {
  292. return this.trimUrl(aUrl, host + "/" + aLabel, aSeq + 1);
  293. }
  294. aSeq++;
  295. }
  296. // Use the whole url spec but ignoring the reference.
  297. if (aSeq == 5) {
  298. return this.trimUrl(aUrl, aUrl.specIgnoringRef, aSeq + 1);
  299. }
  300. // Give up.
  301. return aUrl.spec;
  302. },
  303. parseSource: function (aDebuggerView, aParser) {
  304. let editor = aDebuggerView.editor;
  305. let contents = editor.getText();
  306. let location = aDebuggerView.Sources.selectedValue;
  307. let parsedSource = aParser.get(contents, location);
  308. return parsedSource;
  309. },
  310. findIdentifier: function (aEditor, parsedSource, x, y) {
  311. let editor = aEditor;
  312. // Calculate the editor's line and column at the current x and y coords.
  313. let hoveredPos = editor.getPositionFromCoords({ left: x, top: y });
  314. let hoveredOffset = editor.getOffset(hoveredPos);
  315. let hoveredLine = hoveredPos.line;
  316. let hoveredColumn = hoveredPos.ch;
  317. let scriptInfo = parsedSource.getScriptInfo(hoveredOffset);
  318. // If the script length is negative, we're not hovering JS source code.
  319. if (scriptInfo.length == -1) {
  320. return;
  321. }
  322. // Using the script offset, determine the actual line and column inside the
  323. // script, to use when finding identifiers.
  324. let scriptStart = editor.getPosition(scriptInfo.start);
  325. let scriptLineOffset = scriptStart.line;
  326. let scriptColumnOffset = (hoveredLine == scriptStart.line ? scriptStart.ch : 0);
  327. let scriptLine = hoveredLine - scriptLineOffset;
  328. let scriptColumn = hoveredColumn - scriptColumnOffset;
  329. let identifierInfo = parsedSource.getIdentifierAt({
  330. line: scriptLine + 1,
  331. column: scriptColumn,
  332. scriptIndex: scriptInfo.index
  333. });
  334. return identifierInfo;
  335. }
  336. };