tor-browser

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

prettyPrint.js (17861B)


      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 import { generatedToOriginalId } from "devtools/client/shared/source-map-loader/index";
      6 
      7 import assert from "../../utils/assert";
      8 import { recordEvent } from "../../utils/telemetry";
      9 import {
     10  updateBreakpointPositionsForNewPrettyPrintedSource,
     11  updateBreakpointsForNewPrettyPrintedSource,
     12 } from "../breakpoints/index";
     13 
     14 import {
     15  getPrettySourceURL,
     16  isJavaScript,
     17  isMinified,
     18 } from "../../utils/source";
     19 import { isFulfilled, fulfilled } from "../../utils/async-value";
     20 import {
     21  getOriginalLocation,
     22  getGeneratedLocation,
     23 } from "../../utils/source-maps";
     24 import { prefs } from "../../utils/prefs";
     25 import {
     26  loadGeneratedSourceText,
     27  loadOriginalSourceText,
     28 } from "./loadSourceText";
     29 import { removeSources } from "./removeSources";
     30 import { mapFrames } from "../pause/index";
     31 import { selectSpecificLocation } from "../sources/index";
     32 import { createPrettyPrintOriginalSource } from "../../client/firefox/create";
     33 
     34 import {
     35  getFirstSourceActorForGeneratedSource,
     36  getSource,
     37  getSelectedLocation,
     38  canPrettyPrintSource,
     39  getSourceTextContentForSource,
     40 } from "../../selectors/index";
     41 
     42 import { selectSource } from "./select";
     43 import { memoizeableAction } from "../../utils/memoizableAction";
     44 
     45 import DevToolsUtils from "devtools/shared/DevToolsUtils";
     46 
     47 /**
     48 * Replace all line breaks with standard \n line breaks for easier parsing.
     49 */
     50 const LINE_BREAK_REGEX = /\r\n?|\n|\u2028|\u2029/g;
     51 function sanitizeLineBreaks(str) {
     52  return str.replace(LINE_BREAK_REGEX, "\n");
     53 }
     54 
     55 /**
     56 * Retrieve all line breaks in the provided string.
     57 * Note: this assumes the line breaks were previously sanitized with
     58 * `sanitizeLineBreaks` defined above.
     59 */
     60 const SIMPLE_LINE_BREAK_REGEX = /\n/g;
     61 function matchAllLineBreaks(str) {
     62  return Array.from(str.matchAll(SIMPLE_LINE_BREAK_REGEX));
     63 }
     64 
     65 function getPrettyOriginalSourceURL(generatedSource) {
     66  return getPrettySourceURL(generatedSource.url || generatedSource.id);
     67 }
     68 
     69 export async function prettyPrintSourceTextContent(
     70  sourceMapLoader,
     71  prettyPrintWorker,
     72  generatedSource,
     73  content,
     74  actors
     75 ) {
     76  if (!content || !isFulfilled(content)) {
     77    throw new Error("Cannot pretty-print a file that has not loaded");
     78  }
     79 
     80  const contentValue = content.value;
     81  if (
     82    (!isJavaScript(generatedSource, contentValue) && !generatedSource.isHTML) ||
     83    contentValue.type !== "text"
     84  ) {
     85    throw new Error(
     86      `Can't prettify ${contentValue.contentType} files, only HTML and Javascript.`
     87    );
     88  }
     89 
     90  const url = getPrettyOriginalSourceURL(generatedSource);
     91 
     92  let prettyPrintWorkerResult;
     93  if (generatedSource.isHTML) {
     94    prettyPrintWorkerResult = await prettyPrintHtmlFile({
     95      prettyPrintWorker,
     96      generatedSource,
     97      content,
     98      actors,
     99    });
    100  } else {
    101    prettyPrintWorkerResult = await prettyPrintWorker.prettyPrint({
    102      sourceText: contentValue.value,
    103      indent: " ".repeat(prefs.indentSize),
    104      url,
    105    });
    106  }
    107 
    108  // The source map URL service used by other devtools listens to changes to
    109  // sources based on their actor IDs, so apply the sourceMap there too.
    110  const generatedSourceIds = [
    111    generatedSource.id,
    112    ...actors.map(item => item.actor),
    113  ];
    114  await sourceMapLoader.setSourceMapForGeneratedSources(
    115    generatedSourceIds,
    116    prettyPrintWorkerResult.sourceMap
    117  );
    118 
    119  return {
    120    text: prettyPrintWorkerResult.code,
    121    contentType: contentValue.contentType,
    122  };
    123 }
    124 
    125 /**
    126 * Pretty print inline script inside an HTML file
    127 *
    128 * @param {object} options
    129 * @param {PrettyPrintDispatcher} options.prettyPrintWorker: The prettyPrint worker
    130 * @param {object} options.generatedSource: The HTML source we want to pretty print
    131 * @param {object} options.content
    132 * @param {Array} options.actors: An array of the HTML file inline script sources data
    133 *
    134 * @returns Promise<Object> A promise that resolves with an object of the following shape:
    135 *                          - {String} code: The prettified HTML text
    136 *                          - {Object} sourceMap: The sourceMap object
    137 */
    138 async function prettyPrintHtmlFile({
    139  prettyPrintWorker,
    140  generatedSource,
    141  content,
    142  actors,
    143 }) {
    144  const url = getPrettyOriginalSourceURL(generatedSource);
    145  const contentValue = content.value;
    146 
    147  // Original source may contain unix-style & windows-style breaks.
    148  // SpiderMonkey works a sanitized version of the source using only \n (unix).
    149  // Sanitize before parsing the source to align with SpiderMonkey.
    150  const htmlFileText = sanitizeLineBreaks(contentValue.value);
    151  const prettyPrintWorkerResult = { code: htmlFileText };
    152 
    153  const allLineBreaks = matchAllLineBreaks(htmlFileText);
    154  let lineCountDelta = 0;
    155 
    156  // Sort inline script actors so they are in the same order as in the html document.
    157  actors.sort((a, b) => {
    158    if (a.sourceStartLine === b.sourceStartLine) {
    159      return a.sourceStartColumn > b.sourceStartColumn;
    160    }
    161    return a.sourceStartLine > b.sourceStartLine;
    162  });
    163 
    164  const prettyPrintTaskId = generatedSource.id;
    165 
    166  // We don't want to replace part of the HTML document in the loop since it would require
    167  // to account for modified lines for each iteration.
    168  // Instead, we'll put each sections to replace in this array, where elements will be
    169  // objects of the following shape:
    170  // {Integer} startIndex: The start index in htmlFileText of the section we want to replace
    171  // {Integer} endIndex: The end index in htmlFileText of the section we want to replace
    172  // {String} prettyText: The pretty text we'll replace the original section with
    173  // Once we iterated over all the inline scripts, we'll do the replacements (on the html
    174  // file text) in reverse order, so we don't need have to care about the modified lines
    175  // for each iteration.
    176  const replacements = [];
    177 
    178  const seenLocations = new Set();
    179 
    180  for (const sourceInfo of actors) {
    181    // We can get duplicate source actors representing the same inline script which will
    182    // cause trouble in the pretty printing here. This should be fixed on the server (see
    183    // Bug 1824979), but in the meantime let's not handle the same location twice so the
    184    // pretty printing is not impacted.
    185    const location = `${sourceInfo.sourceStartLine}:${sourceInfo.sourceStartColumn}`;
    186    if (!sourceInfo.sourceLength || seenLocations.has(location)) {
    187      continue;
    188    }
    189    seenLocations.add(location);
    190    // Here we want to get the index of the last line break before the script tag.
    191    // In allLineBreaks, this would be the item at (script tag line - 1)
    192    // Since sourceInfo.sourceStartLine is 1-based, we need to get the item at (sourceStartLine - 2)
    193    const indexAfterPreviousLineBreakInHtml =
    194      sourceInfo.sourceStartLine > 1
    195        ? allLineBreaks[sourceInfo.sourceStartLine - 2].index + 1
    196        : 0;
    197 
    198    // The `sourceStartColumn` refers to final unicode characters column (including 16-bits characters),
    199    // not including any unicode characters encoded by surrogate pairs (two 16 bit code units)
    200    // i.e outside of the Basic Multiligual Plane. So calculate and add those characters to the looked-up start index.
    201    const startColumn =
    202      indexAfterPreviousLineBreakInHtml + sourceInfo.sourceStartColumn;
    203    const htmlBeforeStr = htmlFileText.substring(0, startColumn);
    204    const codeUnitLength = htmlBeforeStr.length,
    205      codePointLength = [...htmlBeforeStr].length;
    206    const extraCharsWithForStrTwoCodeUnits = codeUnitLength - codePointLength;
    207 
    208    const startIndex = startColumn + extraCharsWithForStrTwoCodeUnits;
    209    const endIndex = startIndex + sourceInfo.sourceLength;
    210    const scriptText = htmlFileText.substring(startIndex, endIndex);
    211    DevToolsUtils.assert(
    212      scriptText.length == sourceInfo.sourceLength,
    213      "script text has expected length"
    214    );
    215 
    216    // Here we're going to pretty print each inline script content.
    217    // Since we want to have a sourceMap that we'll apply to the whole HTML file,
    218    // we'll only collect the sourceMap once we handled all inline scripts.
    219    // `taskId` allows us to signal to the worker that all those calls are part of the
    220    // same bigger file, and we'll use it later to get the sourceMap.
    221    const prettyText = await prettyPrintWorker.prettyPrintInlineScript({
    222      taskId: prettyPrintTaskId,
    223      sourceText: scriptText,
    224      indent: " ".repeat(prefs.indentSize),
    225      url,
    226      originalStartLine: sourceInfo.sourceStartLine,
    227      originalStartColumn: sourceInfo.sourceStartColumn,
    228      // The generated line will be impacted by the previous inline scripts that were
    229      // pretty printed, which is why we offset with lineCountDelta
    230      generatedStartLine: sourceInfo.sourceStartLine + lineCountDelta,
    231      generatedStartColumn: sourceInfo.sourceStartColumn,
    232      lineCountDelta,
    233    });
    234 
    235    // We need to keep track of the line added/removed in order to properly offset
    236    // the mapping of the pretty-print text
    237    lineCountDelta +=
    238      matchAllLineBreaks(prettyText).length -
    239      matchAllLineBreaks(scriptText).length;
    240 
    241    replacements.push({
    242      startIndex,
    243      endIndex,
    244      prettyText,
    245    });
    246  }
    247 
    248  // `getSourceMap` allow us to collect the computed source map resulting of the calls
    249  // to `prettyPrint` with the same taskId.
    250  prettyPrintWorkerResult.sourceMap =
    251    await prettyPrintWorker.getSourceMap(prettyPrintTaskId);
    252 
    253  // Sort replacement in reverse order so we can replace code in the HTML file more easily
    254  replacements.sort((a, b) => a.startIndex < b.startIndex);
    255  for (const { startIndex, endIndex, prettyText } of replacements) {
    256    prettyPrintWorkerResult.code =
    257      prettyPrintWorkerResult.code.substring(0, startIndex) +
    258      prettyText +
    259      prettyPrintWorkerResult.code.substring(endIndex);
    260  }
    261 
    262  return prettyPrintWorkerResult;
    263 }
    264 
    265 function createPrettySource(source, sourceActor) {
    266  return async ({ dispatch }) => {
    267    const url = getPrettyOriginalSourceURL(source);
    268    const id = generatedToOriginalId(source.id, url);
    269    const prettySource = createPrettyPrintOriginalSource(id, url, source);
    270 
    271    dispatch({
    272      type: "ADD_ORIGINAL_SOURCES",
    273      originalSources: [prettySource],
    274      generatedSourceActor: sourceActor,
    275    });
    276    return prettySource;
    277  };
    278 }
    279 
    280 function selectPrettyLocation(prettySource) {
    281  return async thunkArgs => {
    282    const { dispatch, getState } = thunkArgs;
    283    let location = getSelectedLocation(getState());
    284 
    285    // If we were selecting a particular line in the minified/generated source,
    286    // try to select the matching line in the prettified/original source.
    287    if (
    288      location &&
    289      location.line >= 1 &&
    290      getPrettySourceURL(location.source.url) == prettySource.url
    291    ) {
    292      // Note that it requires to have called `prettyPrintSourceTextContent` and `sourceMapLoader.setSourceMapForGeneratedSources`
    293      // to be functional and so to be called after `loadOriginalSourceText` completed.
    294      location = await getOriginalLocation(location, thunkArgs);
    295 
    296      // If the precise line/column correctly mapped to the pretty printed source, select that precise location.
    297      // Otherwise fallback to selectSource in order to select the first line instead of the current line within the bundle.
    298      if (location.source == prettySource) {
    299        return dispatch(selectSpecificLocation(location));
    300      }
    301    }
    302 
    303    return dispatch(selectSource(prettySource));
    304  };
    305 }
    306 
    307 /**
    308 * Toggle the pretty printing of a source's text.
    309 * Nothing will happen for non-javascript, non-minified, or files that can't be pretty printed.
    310 *
    311 * @param Object source
    312 *        The source object for the minified/generated source.
    313 * @param Boolean isAutoPrettyPrinting
    314 *        Are we pretty printing this source because of auto-pretty printing preference?
    315 * @returns Promise
    316 *          A promise that resolves to the Pretty print/original source object.
    317 */
    318 export async function doPrettyPrintSource(
    319  source,
    320  isAutoPrettyPrinting,
    321  thunkArgs
    322 ) {
    323  const { dispatch, getState } = thunkArgs;
    324  recordEvent("pretty_print");
    325 
    326  assert(
    327    !source.isOriginal,
    328    "Pretty-printing only allowed on generated sources"
    329  );
    330 
    331  const sourceActor = getFirstSourceActorForGeneratedSource(
    332    getState(),
    333    source.id
    334  );
    335 
    336  await dispatch(loadGeneratedSourceText(sourceActor));
    337 
    338  // Just after having retrieved the minimized text content,
    339  // verify if the source can really be pretty printed.
    340  // In case it can't, revert the pretty printed status on the minimized source.
    341  // This is especially useful when automatic pretty printing is enabled.
    342  if (
    343    isAutoPrettyPrinting &&
    344    (!canPrettyPrintSource(getState(), source, sourceActor) ||
    345      !isMinified(
    346        source,
    347        getSourceTextContentForSource(getState(), source, sourceActor)
    348      ))
    349  ) {
    350    dispatch({
    351      type: "REMOVE_PRETTY_PRINTED_SOURCE",
    352      source,
    353    });
    354    return null;
    355  }
    356 
    357  const newPrettySource = await dispatch(
    358    createPrettySource(source, sourceActor)
    359  );
    360 
    361  // Force loading the pretty source/original text.
    362  // This will end up calling prettyPrintSourceTextContent() of this module, and
    363  // more importantly, will populate the sourceMapLoader, which is used by selectPrettyLocation.
    364  await dispatch(loadOriginalSourceText(newPrettySource));
    365 
    366  // Update frames to the new pretty/original source (in case we were paused).
    367  // Map the frames before selecting the pretty source in order to avoid
    368  // having bundle/generated source for frames (we may compute scope things for the bundle).
    369  await dispatch(mapFrames(sourceActor.thread));
    370 
    371  // The original locations of any stored breakpoint positions need to be updated
    372  // to point to the new pretty source.
    373  await dispatch(updateBreakpointPositionsForNewPrettyPrintedSource(source));
    374 
    375  // Update breakpoints locations to the new pretty/original source
    376  await dispatch(updateBreakpointsForNewPrettyPrintedSource(source));
    377 
    378  // A mutated flag, only meant to be used within this module
    379  // to know when we are done loading the pretty printed source.
    380  // This is important for the callsite in `selectLocation`
    381  // in order to ensure all action are completed and especially `mapFrames`.
    382  // Otherwise we may use generated frames there.
    383  newPrettySource._loaded = true;
    384 
    385  return fulfilled(newPrettySource);
    386 }
    387 
    388 // Use memoization in order to allow calling this actions many times
    389 // while ensuring creating the pretty source only once.
    390 export const prettyPrintSource = memoizeableAction("prettyPrintSource", {
    391  getValue: ({ source }, { getState }) => {
    392    // Lookup for an already existing pretty source
    393    const url = getPrettyOriginalSourceURL(source);
    394    const id = generatedToOriginalId(source.id, url);
    395    const s = getSource(getState(), id);
    396    // Avoid returning it if doTogglePrettyPrint isn't completed.
    397    if (!s || !s._loaded) {
    398      return undefined;
    399    }
    400    return fulfilled(s);
    401  },
    402  createKey: ({ source }) => source.id,
    403  action: ({ source, isAutoPrettyPrinting = false }, thunkArgs) =>
    404    doPrettyPrintSource(source, isAutoPrettyPrinting, thunkArgs),
    405 });
    406 
    407 export function prettyPrintAndSelectSource(source) {
    408  return async ({ dispatch }) => {
    409    const prettySource = await dispatch(prettyPrintSource({ source }));
    410 
    411    // Select the pretty/original source based on the location we may
    412    // have had against the minified/generated source.
    413    // This uses source map to map locations.
    414    // Also note that selecting a location force many things:
    415    // * opening tabs
    416    // * fetching inline scope
    417    // * fetching breakable lines
    418    //
    419    // This isn't part of prettyPrintSource/doPrettyPrintSource
    420    // because if the source is already pretty printed, the memoization
    421    // would avoid trying to update to the mapped location based
    422    // on current location on the minified source.
    423    await dispatch(selectPrettyLocation(prettySource));
    424 
    425    return prettySource;
    426  };
    427 }
    428 
    429 export function removePrettyPrintedSource(source) {
    430  return async thunkArgs => {
    431    const { getState, dispatch } = thunkArgs;
    432    const { generatedSource } = source;
    433 
    434    let location = getSelectedLocation(getState());
    435    // If we were selecting a particular line in the pretty printed source
    436    // try to select the matching line in the minimized source.
    437    // Map the original to generated location before removing the source as it would clear the mappings.
    438    if (location && location.line >= 1 && location.source == source) {
    439      // Note that it requires to have called `prettyPrintSourceTextContent` and `sourceMapLoader.setSourceMapForGeneratedSources`
    440      // to be functional and so to be called after `loadOriginalSourceText` completed.
    441      location = await getGeneratedLocation(location, thunkArgs);
    442    }
    443 
    444    dispatch({
    445      type: "REMOVE_PRETTY_PRINTED_SOURCE",
    446      source,
    447    });
    448 
    449    // Prevent resetting the currently selected source to avoid blinking.
    450    // The minimized source will be selected right after the reducers are cleaned up
    451    await dispatch(
    452      removeSources([source], [], { resetSelectedLocation: false })
    453    );
    454 
    455    const sourceActor = getFirstSourceActorForGeneratedSource(
    456      getState(),
    457      generatedSource.id
    458    );
    459    // In case we are paused, update frames to remove references to the pretty printed sources
    460    await dispatch(mapFrames(sourceActor.thread));
    461 
    462    // If the precise line/column correctly mapped to the minimized source, select that precise location.
    463    // Otherwise fallback to selectSource in order to select the first line instead of the current line within the pretty version.
    464    if (location.source == generatedSource) {
    465      await dispatch(selectSpecificLocation(location));
    466    } else {
    467      await dispatch(selectSource(generatedSource));
    468    }
    469  };
    470 }