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 }