tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

source.js (10713B)


      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 
      5 /**
      6 * Utils for working with Source URLs
      7 *
      8 * @module utils/source
      9 */
     10 
     11 const {
     12  getUnicodeUrl,
     13 } = require("resource://devtools/client/shared/unicode-url.js");
     14 const {
     15  micromatch,
     16 } = require("resource://devtools/client/shared/vendor/micromatch/micromatch.js");
     17 
     18 import { getRelativePath } from "../utils/sources-tree/utils";
     19 import { endTruncateStr } from "./utils";
     20 import { truncateMiddleText } from "../utils/text";
     21 import { memoizeLast } from "../utils/memoizeLast";
     22 import { toWasmSourceLine, getEditor } from "./editor/index";
     23 export { isMinified } from "./isMinified";
     24 
     25 import { isFulfilled } from "./async-value";
     26 
     27 export const sourceTypes = {
     28  coffee: "coffeescript",
     29  js: "javascript",
     30  jsx: "react",
     31  ts: "typescript",
     32  tsx: "typescript",
     33  vue: "vue",
     34 };
     35 
     36 export const javascriptLikeExtensions = new Set(["marko", "es6", "vue", "jsm"]);
     37 
     38 function getPath(source) {
     39  const { path } = source.displayURL;
     40  let lastIndex = path.lastIndexOf("/");
     41  let nextToLastIndex = path.lastIndexOf("/", lastIndex - 1);
     42 
     43  const result = [];
     44  do {
     45    result.push(path.slice(nextToLastIndex + 1, lastIndex));
     46    lastIndex = nextToLastIndex;
     47    nextToLastIndex = path.lastIndexOf("/", lastIndex - 1);
     48  } while (lastIndex !== nextToLastIndex);
     49 
     50  result.push("");
     51 
     52  return result;
     53 }
     54 
     55 export function shouldBlackbox(source) {
     56  if (!source) {
     57    return false;
     58  }
     59 
     60  if (!source.url) {
     61    return false;
     62  }
     63 
     64  return true;
     65 }
     66 
     67 /**
     68 * Checks if the frame is within a line ranges which are blackboxed
     69 * in the source.
     70 *
     71 * @param {object}  frame
     72 *                  The current frame
     73 * @param {object}  blackboxedRanges
     74 *                  The currently blackboxedRanges for all the sources.
     75 * @param {boolean} isFrameBlackBoxed
     76 *                  If the frame is within the blackboxed range
     77 *                  or not.
     78 */
     79 export function isFrameBlackBoxed(frame, blackboxedRanges) {
     80  const { source } = frame.location;
     81  return (
     82    !!blackboxedRanges[source.url] &&
     83    (!blackboxedRanges[source.url].length ||
     84      !!findBlackBoxRange(source, blackboxedRanges, {
     85        start: frame.location.line,
     86        end: frame.location.line,
     87      }))
     88  );
     89 }
     90 
     91 /**
     92 * Checks if a blackbox range exist for the line range.
     93 * That is if any start and end lines overlap any of the
     94 * blackbox ranges
     95 *
     96 * @param {object}  source
     97 *                  The current selected source
     98 * @param {object}  blackboxedRanges
     99 *                  The store of blackboxedRanges
    100 * @param {object}  lineRange
    101 *                  The start/end line range `{ start: <Number>, end: <Number> }`
    102 * @return {object} blackboxRange
    103 *                  The first matching blackbox range that all or part of the
    104 *                  specified lineRange sits within.
    105 */
    106 export function findBlackBoxRange(source, blackboxedRanges, lineRange) {
    107  const ranges = blackboxedRanges[source.url];
    108  if (!ranges || !ranges.length) {
    109    return null;
    110  }
    111 
    112  return ranges.find(
    113    range =>
    114      (lineRange.start >= range.start.line &&
    115        lineRange.start <= range.end.line) ||
    116      (lineRange.end >= range.start.line && lineRange.end <= range.end.line)
    117  );
    118 }
    119 
    120 /**
    121 * Checks if a source line is blackboxed
    122 *
    123 * @param {Array} ranges - Line ranges that are blackboxed
    124 * @param {number} line
    125 * @param {boolean} isSourceOnIgnoreList - is the line in a source that is on
    126 *                                         the sourcemap ignore lists then the line is blackboxed.
    127 * @returns boolean
    128 */
    129 export function isLineBlackboxed(ranges, line, isSourceOnIgnoreList) {
    130  if (isSourceOnIgnoreList) {
    131    return true;
    132  }
    133 
    134  if (!ranges) {
    135    return false;
    136  }
    137  // If the whole source is ignored , then the line is
    138  // ignored.
    139  if (!ranges.length) {
    140    return true;
    141  }
    142  return !!ranges.find(
    143    range => line >= range.start.line && line <= range.end.line
    144  );
    145 }
    146 
    147 /**
    148 * Returns true if the specified url and/or content type are specific to
    149 * javascript files.
    150 *
    151 * @return boolean
    152 *         True if the source is likely javascript.
    153 *
    154 * @memberof utils/source
    155 * @static
    156 */
    157 export function isJavaScript(source, content) {
    158  const extension = source.displayURL.fileExtension;
    159  const contentType = content.type === "wasm" ? null : content.contentType;
    160  return (
    161    javascriptLikeExtensions.has(extension) ||
    162    !!(contentType && contentType.includes("javascript"))
    163  );
    164 }
    165 
    166 export function isPrettyURL(url) {
    167  return url ? url.endsWith(":formatted") : false;
    168 }
    169 
    170 /**
    171 * @memberof utils/source
    172 * @static
    173 */
    174 export function getPrettySourceURL(url) {
    175  if (!url) {
    176    url = "";
    177  }
    178  return `${url}:formatted`;
    179 }
    180 
    181 /**
    182 * @memberof utils/source
    183 * @static
    184 */
    185 export function getRawSourceURL(url) {
    186  return url && url.endsWith(":formatted")
    187    ? url.slice(0, -":formatted".length)
    188    : url;
    189 }
    190 
    191 function resolveFileURL(
    192  url,
    193  transformUrl = initialUrl => initialUrl,
    194  truncate = true
    195 ) {
    196  url = getRawSourceURL(url || "");
    197  const name = transformUrl(url);
    198  if (!truncate) {
    199    return name;
    200  }
    201  return endTruncateStr(name, 50);
    202 }
    203 
    204 export function getFormattedSourceId(id) {
    205  if (typeof id != "string") {
    206    console.error(
    207      "Expected source id to be a string, got",
    208      typeof id,
    209      " | id:",
    210      id
    211    );
    212    return "";
    213  }
    214  return id.substring(id.lastIndexOf("/") + 1);
    215 }
    216 
    217 /**
    218 * Provides a middle-truncated filename displayed in Tab titles
    219 */
    220 export function getTruncatedFileName(source) {
    221  return truncateMiddleText(source.longName, 30);
    222 }
    223 
    224 /**
    225 * Gets path for files with same filename for editor tabs, breakpoints, etc.
    226 * Pass the source, and list of other sources
    227 *
    228 * @memberof utils/source
    229 * @static
    230 */
    231 
    232 export function getDisplayPath(mySource, sources) {
    233  const rawSourceURL = getRawSourceURL(mySource.url);
    234  const filename = mySource.shortName;
    235 
    236  // Find sources that have the same filename, but different paths
    237  // as the original source
    238  const similarSources = sources.filter(source => {
    239    const rawSource = getRawSourceURL(source.url);
    240    return rawSourceURL != rawSource && filename == source.shortName;
    241  });
    242 
    243  if (!similarSources.length) {
    244    return undefined;
    245  }
    246 
    247  // get an array of source path directories e.g. ['a/b/c.html'] => [['b', 'a']]
    248  const paths = new Array(similarSources.length + 1);
    249 
    250  paths[0] = getPath(mySource);
    251  for (let i = 0; i < similarSources.length; ++i) {
    252    paths[i + 1] = getPath(similarSources[i]);
    253  }
    254 
    255  // create an array of similar path directories and one dis-similar directory
    256  // for example [`a/b/c.html`, `a1/b/c.html`] => ['b', 'a']
    257  // where 'b' is the similar directory and 'a' is the dis-similar directory.
    258  let displayPath = "";
    259  for (let i = 0; i < paths[0].length; i++) {
    260    let similar = false;
    261    for (let k = 1; k < paths.length; ++k) {
    262      if (paths[k][i] === paths[0][i]) {
    263        similar = true;
    264        break;
    265      }
    266    }
    267 
    268    displayPath = paths[0][i] + (i !== 0 ? "/" : "") + displayPath;
    269 
    270    if (!similar) {
    271      break;
    272    }
    273  }
    274 
    275  return displayPath;
    276 }
    277 
    278 /**
    279 * Gets a readable source URL for display purposes.
    280 * If the source does not have a URL, the source ID will be returned instead.
    281 *
    282 * @memberof utils/source
    283 * @static
    284 */
    285 export function getFileURL(source, truncate = true) {
    286  const { url, id } = source;
    287  if (!url) {
    288    return getFormattedSourceId(id);
    289  }
    290 
    291  return resolveFileURL(url, getUnicodeUrl, truncate);
    292 }
    293 
    294 function getNthLine(str, lineNum) {
    295  let startIndex = -1;
    296 
    297  let newLinesFound = 0;
    298  while (newLinesFound < lineNum) {
    299    const nextIndex = str.indexOf("\n", startIndex + 1);
    300    if (nextIndex === -1) {
    301      return null;
    302    }
    303    startIndex = nextIndex;
    304    newLinesFound++;
    305  }
    306  const endIndex = str.indexOf("\n", startIndex + 1);
    307  if (endIndex === -1) {
    308    return str.slice(startIndex + 1);
    309  }
    310 
    311  return str.slice(startIndex + 1, endIndex);
    312 }
    313 
    314 export const getLineText = memoizeLast((sourceId, asyncContent, line) => {
    315  if (!asyncContent || !isFulfilled(asyncContent)) {
    316    return "";
    317  }
    318 
    319  const content = asyncContent.value;
    320 
    321  if (content.type === "wasm") {
    322    const editor = getEditor();
    323    const lines = editor.renderWasmText(content);
    324    return lines[toWasmSourceLine(line)] || "";
    325  }
    326 
    327  const lineText = getNthLine(content.value, line - 1);
    328  return lineText || "";
    329 });
    330 
    331 export function getTextAtPosition(sourceId, asyncContent, location) {
    332  const { column, line = 0 } = location;
    333 
    334  const lineText = getLineText(sourceId, asyncContent, line);
    335  return lineText.slice(column, column + 100).trim();
    336 }
    337 
    338 /**
    339 * Compute the CSS classname string to use for the icon of a given source.
    340 *
    341 * @param {object} source
    342 *        The reducer source object.
    343 * @param {boolean} isBlackBoxed
    344 *        To be set to true, when the given source is blackboxed.
    345 *        but another tab for that source is opened pretty printed.
    346 * @return String
    347 *        The classname to use.
    348 */
    349 export function getSourceClassnames(source, isBlackBoxed) {
    350  // Conditionals should be ordered by priority of icon!
    351  const defaultClassName = "file";
    352 
    353  if (!source || !source.url) {
    354    return defaultClassName;
    355  }
    356 
    357  if (isBlackBoxed) {
    358    return "blackBox";
    359  }
    360 
    361  if (isUrlExtension(source.url)) {
    362    return "extension";
    363  }
    364 
    365  return sourceTypes[source.displayURL.fileExtension] || defaultClassName;
    366 }
    367 
    368 export function getRelativeUrl(source, root) {
    369  const { group, path } = source.displayURL;
    370  if (!root) {
    371    return path;
    372  }
    373 
    374  // + 1 removes the leading "/"
    375  const url = group + path;
    376  return url.slice(url.indexOf(root) + root.length + 1);
    377 }
    378 
    379 export function isUrlExtension(url) {
    380  return url.includes("moz-extension:") || url.includes("chrome-extension");
    381 }
    382 
    383 /**
    384 * Checks that source url matches one of the glob patterns
    385 *
    386 * @param {object} source
    387 * @param {string} excludePatterns
    388                  String of comma-seperated glob patterns
    389 * @return {return} Boolean value specifies if the string matches any
    390                 of the patterns.
    391 */
    392 export function matchesGlobPatterns(source, excludePatterns) {
    393  if (!excludePatterns) {
    394    return false;
    395  }
    396  const patterns = excludePatterns
    397    .split(",")
    398    .map(pattern => pattern.trim())
    399    .filter(pattern => pattern !== "");
    400 
    401  if (!patterns.length) {
    402    return false;
    403  }
    404 
    405  return micromatch.contains(
    406    // Makes sure we format the url or id exactly the way its displayed in the search ui,
    407    // as user wil usually create glob patterns based on what is seen in the ui.
    408    source.url ? getRelativePath(source.url) : getFormattedSourceId(source.id),
    409    patterns
    410  );
    411 }