tor-browser

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

pretty-fast.js (32604B)


      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 /* eslint-disable complexity */
      6 
      7 var acorn = require("acorn");
      8 var sourceMap = require("source-map");
      9 const NEWLINE_CODE = 10;
     10 
     11 export function prettyFast(input, options) {
     12  return new PrettyFast(options).getPrettifiedCodeAndSourceMap(input);
     13 }
     14 
     15 // If any of these tokens are seen before a "[" token, we know that "[" token
     16 // is the start of an array literal, rather than a property access.
     17 //
     18 // The only exception is "}", which would need to be disambiguated by
     19 // parsing. The majority of the time, an open bracket following a closing
     20 // curly is going to be an array literal, so we brush the complication under
     21 // the rug, and handle the ambiguity by always assuming that it will be an
     22 // array literal.
     23 const PRE_ARRAY_LITERAL_TOKENS = new Set([
     24  "typeof",
     25  "void",
     26  "delete",
     27  "case",
     28  "do",
     29  "=",
     30  "in",
     31  "of",
     32  "...",
     33  "{",
     34  "*",
     35  "/",
     36  "%",
     37  "else",
     38  ";",
     39  "++",
     40  "--",
     41  "+",
     42  "-",
     43  "~",
     44  "!",
     45  ":",
     46  "?",
     47  ">>",
     48  ">>>",
     49  "<<",
     50  "||",
     51  "&&",
     52  "<",
     53  ">",
     54  "<=",
     55  ">=",
     56  "instanceof",
     57  "&",
     58  "^",
     59  "|",
     60  "==",
     61  "!=",
     62  "===",
     63  "!==",
     64  ",",
     65  "}",
     66 ]);
     67 
     68 // If any of these tokens are seen before a "{" token, we know that "{" token
     69 // is the start of an object literal, rather than the start of a block.
     70 const PRE_OBJECT_LITERAL_TOKENS = new Set([
     71  "typeof",
     72  "void",
     73  "delete",
     74  "=",
     75  "in",
     76  "of",
     77  "...",
     78  "*",
     79  "/",
     80  "%",
     81  "++",
     82  "--",
     83  "+",
     84  "-",
     85  "~",
     86  "!",
     87  ">>",
     88  ">>>",
     89  "<<",
     90  "<",
     91  ">",
     92  "<=",
     93  ">=",
     94  "instanceof",
     95  "&",
     96  "^",
     97  "|",
     98  "==",
     99  "!=",
    100  "===",
    101  "!==",
    102 ]);
    103 
    104 class PrettyFast {
    105  /**
    106   * @param {object} options: Provides configurability of the pretty printing.
    107   * @param {string} options.url: The URL string of the ugly JS code.
    108   * @param {string} options.indent: The string to indent code by.
    109   * @param {SourceMapGenerator} options.sourceMapGenerator: An optional sourceMapGenerator
    110   *                             the mappings will be added to.
    111   * @param {boolean} options.prefixWithNewLine: When true, the pretty printed code will start
    112   *                  with a line break
    113   * @param {Integer} options.originalStartLine: The line the passed script starts at (1-based).
    114   *                  This is used for inline scripts where we need to account for the lines
    115   *                  before the script tag
    116   * @param {Integer} options.originalStartColumn: The column the passed script starts at (1-based).
    117   *                  This is used for inline scripts where we need to account for the position
    118   *                  of the script tag within the line.
    119   * @param {Integer} options.generatedStartLine: The line where the pretty printed script
    120   *                  will start at (1-based). This is used for pretty printing HTML file,
    121   *                  where we might have handle previous inline scripts that impact the
    122   *                  position of this script.
    123   */
    124  constructor(options = {}) {
    125    // The level of indents deep we are.
    126    this.#indentLevel = 0;
    127    this.#indentChar = options.indent;
    128 
    129    // We will handle mappings between ugly and pretty printed code in this SourceMapGenerator.
    130    this.#sourceMapGenerator =
    131      options.sourceMapGenerator ||
    132      new sourceMap.SourceMapGenerator({
    133        file: options.url,
    134      });
    135 
    136    this.#file = options.url;
    137    this.#hasOriginalStartLine = "originalStartLine" in options;
    138    this.#hasOriginalStartColumn = "originalStartColumn" in options;
    139    this.#hasGeneratedStartLine = "generatedStartLine" in options;
    140    this.#originalStartLine = options.originalStartLine;
    141    this.#originalStartColumn = options.originalStartColumn;
    142    this.#generatedStartLine = options.generatedStartLine;
    143    this.#prefixWithNewLine = options.prefixWithNewLine;
    144  }
    145 
    146  /* options */
    147  #indentChar;
    148  #indentLevel;
    149  #file;
    150  #hasOriginalStartLine;
    151  #hasOriginalStartColumn;
    152  #hasGeneratedStartLine;
    153  #originalStartLine;
    154  #originalStartColumn;
    155  #prefixWithNewLine;
    156  #generatedStartLine;
    157  #sourceMapGenerator;
    158 
    159  /* internals */
    160 
    161  // Whether or not we added a newline on after we added the previous token.
    162  #addedNewline = false;
    163  // Whether or not we added a space after we added the previous token.
    164  #addedSpace = false;
    165  #currentCode = "";
    166  #currentLine = 1;
    167  #currentColumn = 0;
    168  // The tokens parsed by acorn.
    169  #tokenQueue;
    170  // The index of the current token in this.#tokenQueue.
    171  #currentTokenIndex;
    172  // The previous token we added to the pretty printed code.
    173  #previousToken;
    174  // Stack of token types/keywords that can affect whether we want to add a
    175  // newline or a space. We can make that decision based on what token type is
    176  // on the top of the stack. For example, a comma in a parameter list should
    177  // be followed by a space, while a comma in an object literal should be
    178  // followed by a newline.
    179  //
    180  // Strings that go on the stack:
    181  //
    182  //   - "{"
    183  //   - "{\n"
    184  //   - "("
    185  //   - "(\n"
    186  //   - "["
    187  //   - "[\n"
    188  //   - "do"
    189  //   - "?"
    190  //   - "switch"
    191  //   - "case"
    192  //   - "default"
    193  //
    194  // The difference between "[" and "[\n" (as well as "{" and "{\n", and "(" and "(\n")
    195  // is that "\n" is used when we are treating (curly) brackets/parens as line delimiters
    196  // and should increment and decrement the indent level when we find them.
    197  // "[" can represent either a property access (e.g. `x["hi"]`), or an empty array literal
    198  // "{" only represents an empty object literals
    199  // "(" can represent lots of different things (wrapping expression, if/loop condition, function call, …)
    200  #stack = [];
    201 
    202  /**
    203   * @param {string} input: The ugly JS code we want to pretty print.
    204   * @returns {object}
    205   *          An object with the following properties:
    206   *            - code: The pretty printed code string.
    207   *            - map: A SourceMapGenerator instance.
    208   */
    209  getPrettifiedCodeAndSourceMap(input) {
    210    // Add the initial new line if needed
    211    if (this.#prefixWithNewLine) {
    212      this.#write("\n");
    213    }
    214 
    215    // Pass through acorn's tokenizer and append tokens and comments into a
    216    // single queue to process.  For example, the source file:
    217    //
    218    //     foo
    219    //     // a
    220    //     // b
    221    //     bar
    222    //
    223    // After this process, tokenQueue has the following token stream:
    224    //
    225    //     [ foo, '// a', '// b', bar]
    226    this.#tokenQueue = this.#getTokens(input);
    227 
    228    for (let i = 0, len = this.#tokenQueue.length; i < len; i++) {
    229      this.#currentTokenIndex = i;
    230      const token = this.#tokenQueue[i];
    231      const nextToken = this.#tokenQueue[i + 1];
    232      this.#handleToken(token, nextToken);
    233 
    234      // Acorn's tokenizer re-uses tokens, so we have to copy the previous token on
    235      // every iteration. We follow acorn's lead here, and reuse the previousToken
    236      // object the same way that acorn reuses the token object. This allows us
    237      // to avoid allocations and minimize GC pauses.
    238      if (!this.#previousToken) {
    239        this.#previousToken = { loc: { start: {}, end: {} } };
    240      }
    241      this.#previousToken.start = token.start;
    242      this.#previousToken.end = token.end;
    243      this.#previousToken.loc.start.line = token.loc.start.line;
    244      this.#previousToken.loc.start.column = token.loc.start.column;
    245      this.#previousToken.loc.end.line = token.loc.end.line;
    246      this.#previousToken.loc.end.column = token.loc.end.column;
    247      this.#previousToken.type = token.type;
    248      this.#previousToken.value = token.value;
    249    }
    250 
    251    return { code: this.#currentCode, map: this.#sourceMapGenerator };
    252  }
    253 
    254  /**
    255   * Write a pretty printed string to the prettified string and for tokens, add their
    256   * mapping to the SourceMapGenerator.
    257   *
    258   * @param String str
    259   *        The string to be added to the result.
    260   * @param Number line
    261   *        The line number the string came from in the ugly source.
    262   * @param Number column
    263   *        The column number the string came from in the ugly source.
    264   * @param Boolean isToken
    265   *        Set to true when writing tokens, so we can differentiate them from the
    266   *        whitespace we add.
    267   */
    268  #write(str, line, column, isToken) {
    269    this.#currentCode += str;
    270    if (isToken) {
    271      this.#sourceMapGenerator.addMapping({
    272        source: this.#file,
    273        // We need to swap original and generated locations, as the prettified text should
    274        // be seen by the sourcemap service as the "original" one.
    275        generated: {
    276          // originalStartLine is 1-based, and here we just want to offset by a number of
    277          // lines, so we need to decrement it
    278          line: this.#hasOriginalStartLine
    279            ? line + (this.#originalStartLine - 1)
    280            : line,
    281          // We only need to adjust the column number if we're looking at the first line, to
    282          // account for the html text before the opening <script> tag.
    283          column:
    284            line == 1 && this.#hasOriginalStartColumn
    285              ? column + this.#originalStartColumn
    286              : column,
    287        },
    288        original: {
    289          // generatedStartLine is 1-based, and here we just want to offset by a number of
    290          // lines, so we need to decrement it.
    291          line: this.#hasGeneratedStartLine
    292            ? this.#currentLine + (this.#generatedStartLine - 1)
    293            : this.#currentLine,
    294          column: this.#currentColumn,
    295        },
    296        name: null,
    297      });
    298    }
    299 
    300    for (let idx = 0, length = str.length; idx < length; idx++) {
    301      if (str.charCodeAt(idx) === NEWLINE_CODE) {
    302        this.#currentLine++;
    303        this.#currentColumn = 0;
    304      } else {
    305        this.#currentColumn++;
    306      }
    307    }
    308  }
    309 
    310  /**
    311   * Add the given token to the pretty printed results.
    312   *
    313   * @param Object token
    314   *        The token to add.
    315   */
    316  #writeToken(token) {
    317    if (token.type.label == "string") {
    318      this.#write(
    319        `'${stringSanitize(token.value)}'`,
    320        token.loc.start.line,
    321        token.loc.start.column,
    322        true
    323      );
    324    } else if (token.type.label == "template") {
    325      // The backticks, '${', '}' and the template literal's string content are
    326      // all separate tokens.
    327      //
    328      // For example, `AAA${BBB}CCC` becomes the following token sequence,
    329      // where the first template's token.value being 'AAA' and the second
    330      // template's token.value being 'CCC'.
    331      //
    332      //  * token.type.label == '`'
    333      //  * token.type.label == 'template'
    334      //  * token.type.label == '${'
    335      //  * token.type.label == 'name'
    336      //  * token.type.label == '}'
    337      //  * token.type.label == 'template'
    338      //  * token.type.label == '`'
    339      //
    340      // So, just sanitize the token.value without enclosing with backticks.
    341      this.#write(
    342        templateSanitize(token.value),
    343        token.loc.start.line,
    344        token.loc.start.column,
    345        true
    346      );
    347    } else if (token.type.label == "regexp") {
    348      this.#write(
    349        String(token.value.value),
    350        token.loc.start.line,
    351        token.loc.start.column,
    352        true
    353      );
    354    } else {
    355      let value;
    356      if (token.value != null) {
    357        value = token.value;
    358        if (token.type.label === "privateId") {
    359          value = `#${value}`;
    360        }
    361      } else {
    362        value = token.type.label;
    363      }
    364      this.#write(
    365        String(value),
    366        token.loc.start.line,
    367        token.loc.start.column,
    368        true
    369      );
    370    }
    371  }
    372 
    373  /**
    374   * Returns the tokens computed with acorn.
    375   *
    376   * @param String input
    377   *        The JS code we want the tokens of.
    378   * @returns Array<Object>
    379   */
    380  #getTokens(input) {
    381    const tokens = [];
    382 
    383    const res = acorn.tokenizer(input, {
    384      locations: true,
    385      ecmaVersion: "latest",
    386      onComment(block, text, start, end, startLoc, endLoc) {
    387        tokens.push({
    388          type: {},
    389          comment: true,
    390          block,
    391          text,
    392          loc: { start: startLoc, end: endLoc },
    393        });
    394      },
    395    });
    396 
    397    for (;;) {
    398      const token = res.getToken();
    399      tokens.push(token);
    400      if (token.type.label == "eof") {
    401        break;
    402      }
    403    }
    404 
    405    return tokens;
    406  }
    407 
    408  /**
    409   * Add the required whitespace before this token, whether that is a single
    410   * space, newline, and/or the indent on fresh lines.
    411   *
    412   * @param Object token
    413   *        The token we are currently handling.
    414   * @param {object | undefined} nextToken
    415   *        The next token, might not exist if we're on the last token
    416   */
    417  #handleToken(token, nextToken) {
    418    if (token.comment) {
    419      let commentIndentLevel = this.#indentLevel;
    420      if (this.#previousToken?.loc?.end?.line == token.loc.start.line) {
    421        commentIndentLevel = 0;
    422        this.#write(" ");
    423      }
    424      this.#addComment(
    425        commentIndentLevel,
    426        token.block,
    427        token.text,
    428        token.loc.start.line,
    429        nextToken
    430      );
    431      return;
    432    }
    433 
    434    // Shorthand for token.type.keyword, so we don't have to repeatedly access
    435    // properties.
    436    const ttk = token.type.keyword;
    437 
    438    if (ttk && this.#previousToken?.type?.label == ".") {
    439      token.type = acorn.tokTypes.name;
    440    }
    441 
    442    // Shorthand for token.type.label, so we don't have to repeatedly access
    443    // properties.
    444    const ttl = token.type.label;
    445 
    446    if (ttl == "eof") {
    447      if (!this.#addedNewline) {
    448        this.#write("\n");
    449      }
    450      return;
    451    }
    452 
    453    if (belongsOnStack(token)) {
    454      let stackEntry;
    455 
    456      if (isArrayLiteral(token, this.#previousToken)) {
    457        // Don't add new lines for empty array literals
    458        stackEntry = nextToken?.type?.label === "]" ? "[" : "[\n";
    459      } else if (isObjectLiteral(token, this.#previousToken)) {
    460        // Don't add new lines for empty object literals
    461        stackEntry = nextToken?.type?.label === "}" ? "{" : "{\n";
    462      } else if (
    463        isRoundBracketStartingLongParenthesis(
    464          token,
    465          this.#tokenQueue,
    466          this.#currentTokenIndex
    467        )
    468      ) {
    469        stackEntry = "(\n";
    470      } else if (ttl == "{") {
    471        // We need to add a line break for "{" which are not empty object literals
    472        stackEntry = "{\n";
    473      } else {
    474        stackEntry = ttl || ttk;
    475      }
    476 
    477      this.#stack.push(stackEntry);
    478    }
    479 
    480    this.#maybeDecrementIndent(token);
    481    this.#prependWhiteSpace(token);
    482    this.#writeToken(token);
    483    this.#addedSpace = false;
    484 
    485    // If the next token is going to be a comment starting on the same line,
    486    // then no need to add a new line here
    487    if (
    488      !nextToken ||
    489      !nextToken.comment ||
    490      token.loc.end.line != nextToken.loc.start.line
    491    ) {
    492      this.#maybeAppendNewline(token);
    493    }
    494 
    495    this.#maybePopStack(token);
    496    this.#maybeIncrementIndent(token);
    497  }
    498 
    499  /**
    500   * Returns true if the given token should cause us to pop the stack.
    501   */
    502  #maybePopStack(token) {
    503    const ttl = token.type.label;
    504    const ttk = token.type.keyword;
    505    const top = this.#stack.at(-1);
    506 
    507    if (
    508      ttl == "]" ||
    509      ttl == ")" ||
    510      ttl == "}" ||
    511      (ttl == ":" && (top == "case" || top == "default" || top == "?")) ||
    512      (ttk == "while" && top == "do")
    513    ) {
    514      this.#stack.pop();
    515      if (ttl == "}" && this.#stack.at(-1) == "switch") {
    516        this.#stack.pop();
    517      }
    518    }
    519  }
    520 
    521  #maybeIncrementIndent(token) {
    522    if (
    523      // Don't increment indent for empty object literals
    524      (token.type.label == "{" && this.#stack.at(-1) === "{\n") ||
    525      // Don't increment indent for empty array literals
    526      (token.type.label == "[" && this.#stack.at(-1) === "[\n") ||
    527      token.type.keyword == "switch" ||
    528      (token.type.label == "(" && this.#stack.at(-1) === "(\n")
    529    ) {
    530      this.#indentLevel++;
    531    }
    532  }
    533 
    534  #shouldDecrementIndent(token) {
    535    const top = this.#stack.at(-1);
    536    const ttl = token.type.label;
    537    return (
    538      (ttl == "}" && top == "{\n") ||
    539      (ttl == "]" && top == "[\n") ||
    540      (ttl == ")" && top == "(\n")
    541    );
    542  }
    543 
    544  #maybeDecrementIndent(token) {
    545    if (!this.#shouldDecrementIndent(token)) {
    546      return;
    547    }
    548 
    549    const ttl = token.type.label;
    550    this.#indentLevel--;
    551    if (ttl == "}" && this.#stack.at(-2) == "switch") {
    552      this.#indentLevel--;
    553    }
    554  }
    555 
    556  /**
    557   * Add a comment to the pretty printed code.
    558   *
    559   * @param Number indentLevel
    560   *        The number of indents deep we are (might be different from this.#indentLevel).
    561   * @param Boolean block
    562   *        True if the comment is a multiline block style comment.
    563   * @param String text
    564   *        The text of the comment.
    565   * @param Number line
    566   *        The line number to comment appeared on.
    567   * @param Object nextToken
    568   *        The next token if any.
    569   */
    570  #addComment(indentLevel, block, text, line, nextToken) {
    571    const indentString = this.#indentChar.repeat(indentLevel);
    572    const needNewLineAfter =
    573      !block || !(nextToken && nextToken.loc.start.line == line);
    574 
    575    if (block) {
    576      const commentLinesText = text
    577        .split(new RegExp(`/\n${indentString}/`, "g"))
    578        .join(`\n${indentString}`);
    579 
    580      this.#write(
    581        `${indentString}/*${commentLinesText}*/${needNewLineAfter ? "\n" : " "}`
    582      );
    583    } else {
    584      this.#write(`${indentString}//${text}\n`);
    585    }
    586 
    587    this.#addedNewline = needNewLineAfter;
    588    this.#addedSpace = !needNewLineAfter;
    589  }
    590 
    591  /**
    592   * Add the required whitespace before this token, whether that is a single
    593   * space, newline, and/or the indent on fresh lines.
    594   *
    595   * @param Object token
    596   *        The token we are about to add to the pretty printed code.
    597   */
    598  #prependWhiteSpace(token) {
    599    const ttk = token.type.keyword;
    600    const ttl = token.type.label;
    601    let newlineAdded = this.#addedNewline;
    602    let spaceAdded = this.#addedSpace;
    603    const ltt = this.#previousToken?.type?.label;
    604 
    605    // Handle whitespace and newlines after "}" here instead of in
    606    // `isLineDelimiter` because it is only a line delimiter some of the
    607    // time. For example, we don't want to put "else if" on a new line after
    608    // the first if's block.
    609    if (this.#previousToken && ltt == "}") {
    610      if (
    611        (ttk == "while" && this.#stack.at(-1) == "do") ||
    612        needsSpaceBeforeClosingCurlyBracket(ttk)
    613      ) {
    614        this.#write(" ");
    615        spaceAdded = true;
    616      } else if (needsLineBreakBeforeClosingCurlyBracket(ttl)) {
    617        this.#write("\n");
    618        newlineAdded = true;
    619      }
    620    }
    621 
    622    if (
    623      (ttl == ":" && this.#stack.at(-1) == "?") ||
    624      (ttl == "}" && this.#stack.at(-1) == "${")
    625    ) {
    626      this.#write(" ");
    627      spaceAdded = true;
    628    }
    629 
    630    if (this.#previousToken && ltt != "}" && ltt != "." && ttk == "else") {
    631      this.#write(" ");
    632      spaceAdded = true;
    633    }
    634 
    635    const ensureNewline = () => {
    636      if (!newlineAdded) {
    637        this.#write("\n");
    638        newlineAdded = true;
    639      }
    640    };
    641 
    642    if (isASI(token, this.#previousToken)) {
    643      ensureNewline();
    644    }
    645 
    646    if (this.#shouldDecrementIndent(token)) {
    647      ensureNewline();
    648    }
    649 
    650    if (newlineAdded) {
    651      let indentLevel = this.#indentLevel;
    652      if (ttk == "case" || ttk == "default") {
    653        indentLevel--;
    654      }
    655      this.#write(this.#indentChar.repeat(indentLevel));
    656    } else if (!spaceAdded && needsSpaceAfter(token, this.#previousToken)) {
    657      this.#write(" ");
    658      spaceAdded = true;
    659    }
    660  }
    661 
    662  /**
    663   * Append the necessary whitespace to the result after we have added the given
    664   * token.
    665   *
    666   * @param Object token
    667   *        The token that was just added to the result.
    668   */
    669  #maybeAppendNewline(token) {
    670    if (!isLineDelimiter(token, this.#stack)) {
    671      this.#addedNewline = false;
    672      return;
    673    }
    674 
    675    this.#write("\n");
    676    this.#addedNewline = true;
    677  }
    678 }
    679 
    680 /**
    681 * Determines if we think that the given token starts an array literal.
    682 *
    683 * @param Object token
    684 *        The token we want to determine if it is an array literal.
    685 * @param Object previousToken
    686 *        The previous token we added to the pretty printed results.
    687 *
    688 * @returns Boolean
    689 *          True if we believe it is an array literal, false otherwise.
    690 */
    691 function isArrayLiteral(token, previousToken) {
    692  if (token.type.label != "[") {
    693    return false;
    694  }
    695  if (!previousToken) {
    696    return true;
    697  }
    698  if (previousToken.type.isAssign) {
    699    return true;
    700  }
    701 
    702  return PRE_ARRAY_LITERAL_TOKENS.has(
    703    previousToken.type.keyword ||
    704      // Some tokens ('of', 'yield', …) have a `token.type.keyword` of 'name' and their
    705      // actual value in `token.value`
    706      (previousToken.type.label == "name"
    707        ? previousToken.value
    708        : previousToken.type.label)
    709  );
    710 }
    711 
    712 /**
    713 * Determines if we think that the given token starts an object literal.
    714 *
    715 * @param Object token
    716 *        The token we want to determine if it is an object literal.
    717 * @param Object previousToken
    718 *        The previous token we added to the pretty printed results.
    719 *
    720 * @returns Boolean
    721 *          True if we believe it is an object literal, false otherwise.
    722 */
    723 function isObjectLiteral(token, previousToken) {
    724  if (token.type.label != "{") {
    725    return false;
    726  }
    727  if (!previousToken) {
    728    return false;
    729  }
    730  if (previousToken.type.isAssign) {
    731    return true;
    732  }
    733  return PRE_OBJECT_LITERAL_TOKENS.has(
    734    previousToken.type.keyword || previousToken.type.label
    735  );
    736 }
    737 
    738 /**
    739 * Determines if we think that the given token starts a long parenthesis
    740 *
    741 * @param {object} token
    742 *        The token we want to determine if it is the beginning of a long paren.
    743 * @param {Array<object>} tokenQueue
    744 *        The whole list of tokens parsed by acorn
    745 * @param {Integer} currentTokenIndex
    746 *        The index of `token` in `tokenQueue`
    747 * @returns
    748 */
    749 function isRoundBracketStartingLongParenthesis(
    750  token,
    751  tokenQueue,
    752  currentTokenIndex
    753 ) {
    754  if (token.type.label !== "(") {
    755    return false;
    756  }
    757 
    758  // If we're just wrapping an object, we'll have a new line right after
    759  if (tokenQueue[currentTokenIndex + 1].type.label == "{") {
    760    return false;
    761  }
    762 
    763  // We're going to iterate through the following tokens until :
    764  // - we find the closing parent
    765  // - or we reached the maximum character we think should be in parenthesis
    766  const longParentContentLength = 60;
    767 
    768  // Keep track of other parens so we know when we get the closing one for `token`
    769  let parenCount = 0;
    770  let parenContentLength = 0;
    771  for (let i = currentTokenIndex + 1, len = tokenQueue.length; i < len; i++) {
    772    const currToken = tokenQueue[i];
    773    const ttl = currToken.type.label;
    774 
    775    if (ttl == "(") {
    776      parenCount++;
    777    } else if (ttl == ")") {
    778      if (parenCount == 0) {
    779        // Matching closing paren, if we got here, we didn't reach the length limit,
    780        // as we return when parenContentLength is greater than the limit.
    781        return false;
    782      }
    783      parenCount--;
    784    }
    785 
    786    // Aside block comments, all tokens start and end location are on the same line, so
    787    // we can use `start` and `end` to deduce the token length.
    788    const tokenLength = currToken.comment
    789      ? currToken.text.length
    790      : currToken.end - currToken.start;
    791    parenContentLength += tokenLength;
    792 
    793    // If we didn't find the matching closing paren yet and the characters from the
    794    // tokens we evaluated so far are longer than the limit, so consider the token
    795    // a long paren.
    796    if (parenContentLength > longParentContentLength) {
    797      return true;
    798    }
    799  }
    800 
    801  // if we get to here, we didn't found a closing paren, which shouldn't happen
    802  // (scripts with syntax error are not displayed in the debugger), but just to
    803  // be safe, return false.
    804  return false;
    805 }
    806 
    807 // If any of these tokens are followed by a token on a new line, we know that
    808 // ASI cannot happen.
    809 const PREVENT_ASI_AFTER_TOKENS = new Set([
    810  // Binary operators
    811  "*",
    812  "/",
    813  "%",
    814  "+",
    815  "-",
    816  "<<",
    817  ">>",
    818  ">>>",
    819  "<",
    820  ">",
    821  "<=",
    822  ">=",
    823  "instanceof",
    824  "in",
    825  "==",
    826  "!=",
    827  "===",
    828  "!==",
    829  "&",
    830  "^",
    831  "|",
    832  "&&",
    833  "||",
    834  ",",
    835  ".",
    836  "=",
    837  "*=",
    838  "/=",
    839  "%=",
    840  "+=",
    841  "-=",
    842  "<<=",
    843  ">>=",
    844  ">>>=",
    845  "&=",
    846  "^=",
    847  "|=",
    848  // Unary operators
    849  "delete",
    850  "void",
    851  "typeof",
    852  "~",
    853  "!",
    854  "new",
    855  // Function calls and grouped expressions
    856  "(",
    857 ]);
    858 
    859 // If any of these tokens are on a line after the token before it, we know
    860 // that ASI cannot happen.
    861 const PREVENT_ASI_BEFORE_TOKENS = new Set([
    862  // Binary operators
    863  "*",
    864  "/",
    865  "%",
    866  "<<",
    867  ">>",
    868  ">>>",
    869  "<",
    870  ">",
    871  "<=",
    872  ">=",
    873  "instanceof",
    874  "in",
    875  "==",
    876  "!=",
    877  "===",
    878  "!==",
    879  "&",
    880  "^",
    881  "|",
    882  "&&",
    883  "||",
    884  ",",
    885  ".",
    886  "=",
    887  "*=",
    888  "/=",
    889  "%=",
    890  "+=",
    891  "-=",
    892  "<<=",
    893  ">>=",
    894  ">>>=",
    895  "&=",
    896  "^=",
    897  "|=",
    898  // Function calls
    899  "(",
    900 ]);
    901 
    902 /**
    903 * Determine if a token can look like an identifier. More precisely,
    904 * this determines if the token may end or start with a character from
    905 * [A-Za-z0-9_].
    906 *
    907 * @param Object token
    908 *        The token we are looking at.
    909 *
    910 * @returns Boolean
    911 *          True if identifier-like.
    912 */
    913 function isIdentifierLike(token) {
    914  const ttl = token.type.label;
    915  return (
    916    ttl == "name" || ttl == "num" || ttl == "privateId" || !!token.type.keyword
    917  );
    918 }
    919 
    920 /**
    921 * Determines if Automatic Semicolon Insertion (ASI) occurs between these
    922 * tokens.
    923 *
    924 * @param Object token
    925 *        The current token.
    926 * @param Object previousToken
    927 *        The previous token we added to the pretty printed results.
    928 *
    929 * @returns Boolean
    930 *          True if we believe ASI occurs.
    931 */
    932 function isASI(token, previousToken) {
    933  if (!previousToken) {
    934    return false;
    935  }
    936  if (token.loc.start.line === previousToken.loc.start.line) {
    937    return false;
    938  }
    939  if (
    940    previousToken.type.keyword == "return" ||
    941    previousToken.type.keyword == "yield" ||
    942    (previousToken.type.label == "name" && previousToken.value == "yield")
    943  ) {
    944    return true;
    945  }
    946  if (
    947    PREVENT_ASI_AFTER_TOKENS.has(
    948      previousToken.type.label || previousToken.type.keyword
    949    )
    950  ) {
    951    return false;
    952  }
    953  if (PREVENT_ASI_BEFORE_TOKENS.has(token.type.label || token.type.keyword)) {
    954    return false;
    955  }
    956  return true;
    957 }
    958 
    959 /**
    960 * Determine if we should add a newline after the given token.
    961 *
    962 * @param Object token
    963 *        The token we are looking at.
    964 * @param Array stack
    965 *        The stack of open parens/curlies/brackets/etc.
    966 *
    967 * @returns Boolean
    968 *          True if we should add a newline.
    969 */
    970 function isLineDelimiter(token, stack) {
    971  const ttl = token.type.label;
    972  const top = stack.at(-1);
    973  return (
    974    (ttl == ";" && top != "(") ||
    975    // Don't add a new line for empty object literals
    976    (ttl == "{" && top == "{\n") ||
    977    // Don't add a new line for empty array literals
    978    (ttl == "[" && top == "[\n") ||
    979    ((ttl == "," || ttl == "||" || ttl == "&&") && top != "(") ||
    980    (ttl == ":" && (top == "case" || top == "default")) ||
    981    (ttl == "(" && top == "(\n")
    982  );
    983 }
    984 
    985 /**
    986 * Determines if we need to add a space after the token we are about to add.
    987 *
    988 * @param Object token
    989 *        The token we are about to add to the pretty printed code.
    990 * @param Object [previousToken]
    991 *        Optional previous token added to the pretty printed code.
    992 */
    993 function needsSpaceAfter(token, previousToken) {
    994  if (previousToken && needsSpaceBetweenTokens(token, previousToken)) {
    995    return true;
    996  }
    997 
    998  if (token.type.isAssign) {
    999    return true;
   1000  }
   1001  if (token.type.binop != null && previousToken) {
   1002    return true;
   1003  }
   1004  if (token.type.label == "?") {
   1005    return true;
   1006  }
   1007  if (token.type.label == "=>") {
   1008    return true;
   1009  }
   1010 
   1011  return false;
   1012 }
   1013 
   1014 function needsSpaceBeforePreviousToken(previousToken) {
   1015  if (previousToken.type.isLoop) {
   1016    return true;
   1017  }
   1018  if (previousToken.type.isAssign) {
   1019    return true;
   1020  }
   1021  if (previousToken.type.binop != null) {
   1022    return true;
   1023  }
   1024  if (previousToken.value == "of") {
   1025    return true;
   1026  }
   1027 
   1028  const previousTokenTypeLabel = previousToken.type.label;
   1029  if (previousTokenTypeLabel == "?") {
   1030    return true;
   1031  }
   1032  if (previousTokenTypeLabel == ":") {
   1033    return true;
   1034  }
   1035  if (previousTokenTypeLabel == ",") {
   1036    return true;
   1037  }
   1038  if (previousTokenTypeLabel == ";") {
   1039    return true;
   1040  }
   1041  if (previousTokenTypeLabel == "${") {
   1042    return true;
   1043  }
   1044  if (previousTokenTypeLabel == "=>") {
   1045    return true;
   1046  }
   1047  return false;
   1048 }
   1049 
   1050 function isBreakContinueOrReturnStatement(previousTokenKeyword) {
   1051  return (
   1052    previousTokenKeyword == "break" ||
   1053    previousTokenKeyword == "continue" ||
   1054    previousTokenKeyword == "return"
   1055  );
   1056 }
   1057 
   1058 function needsSpaceBeforePreviousTokenKeywordAfterNotDot(previousTokenKeyword) {
   1059  return (
   1060    previousTokenKeyword != "debugger" &&
   1061    previousTokenKeyword != "null" &&
   1062    previousTokenKeyword != "true" &&
   1063    previousTokenKeyword != "false" &&
   1064    previousTokenKeyword != "this" &&
   1065    previousTokenKeyword != "default"
   1066  );
   1067 }
   1068 
   1069 function needsSpaceBeforeClosingParen(tokenTypeLabel) {
   1070  return (
   1071    tokenTypeLabel != ")" &&
   1072    tokenTypeLabel != "]" &&
   1073    tokenTypeLabel != ";" &&
   1074    tokenTypeLabel != "," &&
   1075    tokenTypeLabel != "."
   1076  );
   1077 }
   1078 
   1079 /**
   1080 * Determines if we need to add a space between the previous token we added and
   1081 * the token we are about to add.
   1082 *
   1083 * @param Object token
   1084 *        The token we are about to add to the pretty printed code.
   1085 * @param Object previousToken
   1086 *        The previous token added to the pretty printed code.
   1087 */
   1088 function needsSpaceBetweenTokens(token, previousToken) {
   1089  if (needsSpaceBeforePreviousToken(previousToken)) {
   1090    return true;
   1091  }
   1092 
   1093  const ltt = previousToken.type.label;
   1094  if (ltt == "num" && token.type.label == ".") {
   1095    return true;
   1096  }
   1097 
   1098  const ltk = previousToken.type.keyword;
   1099  const ttl = token.type.label;
   1100  if (ltk != null && ttl != ".") {
   1101    if (isBreakContinueOrReturnStatement(ltk)) {
   1102      return ttl != ";";
   1103    }
   1104    if (needsSpaceBeforePreviousTokenKeywordAfterNotDot(ltk)) {
   1105      return true;
   1106    }
   1107  }
   1108 
   1109  if (ltt == ")" && needsSpaceBeforeClosingParen(ttl)) {
   1110    return true;
   1111  }
   1112 
   1113  if (isIdentifierLike(token) && isIdentifierLike(previousToken)) {
   1114    // We must emit a space to avoid merging the tokens.
   1115    return true;
   1116  }
   1117 
   1118  if (token.type.label == "{" && previousToken.type.label == "name") {
   1119    return true;
   1120  }
   1121 
   1122  return false;
   1123 }
   1124 
   1125 function needsSpaceBeforeClosingCurlyBracket(tokenTypeKeyword) {
   1126  return (
   1127    tokenTypeKeyword == "else" ||
   1128    tokenTypeKeyword == "catch" ||
   1129    tokenTypeKeyword == "finally"
   1130  );
   1131 }
   1132 
   1133 function needsLineBreakBeforeClosingCurlyBracket(tokenTypeLabel) {
   1134  return (
   1135    tokenTypeLabel != "(" &&
   1136    tokenTypeLabel != ";" &&
   1137    tokenTypeLabel != "," &&
   1138    tokenTypeLabel != ")" &&
   1139    tokenTypeLabel != "." &&
   1140    tokenTypeLabel != "template" &&
   1141    tokenTypeLabel != "`"
   1142  );
   1143 }
   1144 
   1145 const commonEscapeCharacters = {
   1146  // Backslash
   1147  "\\": "\\\\",
   1148  // Carriage return
   1149  "\r": "\\r",
   1150  // Tab
   1151  "\t": "\\t",
   1152  // Vertical tab
   1153  "\v": "\\v",
   1154  // Form feed
   1155  "\f": "\\f",
   1156  // Null character
   1157  "\0": "\\x00",
   1158  // Line separator
   1159  "\u2028": "\\u2028",
   1160  // Paragraph separator
   1161  "\u2029": "\\u2029",
   1162 };
   1163 
   1164 const stringEscapeCharacters = {
   1165  ...commonEscapeCharacters,
   1166 
   1167  // Newlines
   1168  "\n": "\\n",
   1169  // Single quotes
   1170  "'": "\\'",
   1171 };
   1172 
   1173 const templateEscapeCharacters = {
   1174  ...commonEscapeCharacters,
   1175 
   1176  // backtick
   1177  "`": "\\`",
   1178 };
   1179 
   1180 const stringRegExpString = `(${Object.values(stringEscapeCharacters).join(
   1181  "|"
   1182 )})`;
   1183 const templateRegExpString = `(${Object.values(templateEscapeCharacters).join(
   1184  "|"
   1185 )})`;
   1186 
   1187 const stringEscapeCharactersRegExp = new RegExp(stringRegExpString, "g");
   1188 const templateEscapeCharactersRegExp = new RegExp(templateRegExpString, "g");
   1189 
   1190 function stringSanitizerReplaceFunc(_, c) {
   1191  return stringEscapeCharacters[c];
   1192 }
   1193 function templateSanitizerReplaceFunc(_, c) {
   1194  return templateEscapeCharacters[c];
   1195 }
   1196 
   1197 /**
   1198 * Make sure that we output the escaped character combination inside string
   1199 * literals instead of various problematic characters.
   1200 */
   1201 function stringSanitize(str) {
   1202  return str.replace(stringEscapeCharactersRegExp, stringSanitizerReplaceFunc);
   1203 }
   1204 function templateSanitize(str) {
   1205  return str.replace(
   1206    templateEscapeCharactersRegExp,
   1207    templateSanitizerReplaceFunc
   1208  );
   1209 }
   1210 
   1211 /**
   1212 * Returns true if the given token type belongs on the stack.
   1213 */
   1214 function belongsOnStack(token) {
   1215  const ttl = token.type.label;
   1216  const ttk = token.type.keyword;
   1217  return (
   1218    ttl == "{" ||
   1219    ttl == "(" ||
   1220    ttl == "[" ||
   1221    ttl == "?" ||
   1222    ttl == "${" ||
   1223    ttk == "do" ||
   1224    ttk == "switch" ||
   1225    ttk == "case" ||
   1226    ttk == "default"
   1227  );
   1228 }