/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at . */
/* eslint-disable complexity */
var acorn = require("acorn");
var sourceMap = require("source-map");
const NEWLINE_CODE = 10;
export function prettyFast(input, options) {
return new PrettyFast(options).getPrettifiedCodeAndSourceMap(input);
}
// If any of these tokens are seen before a "[" token, we know that "[" token
// is the start of an array literal, rather than a property access.
//
// The only exception is "}", which would need to be disambiguated by
// parsing. The majority of the time, an open bracket following a closing
// curly is going to be an array literal, so we brush the complication under
// the rug, and handle the ambiguity by always assuming that it will be an
// array literal.
const PRE_ARRAY_LITERAL_TOKENS = new Set([
"typeof",
"void",
"delete",
"case",
"do",
"=",
"in",
"of",
"...",
"{",
"*",
"/",
"%",
"else",
";",
"++",
"--",
"+",
"-",
"~",
"!",
":",
"?",
">>",
">>>",
"<<",
"||",
"&&",
"<",
">",
"<=",
">=",
"instanceof",
"&",
"^",
"|",
"==",
"!=",
"===",
"!==",
",",
"}",
]);
// If any of these tokens are seen before a "{" token, we know that "{" token
// is the start of an object literal, rather than the start of a block.
const PRE_OBJECT_LITERAL_TOKENS = new Set([
"typeof",
"void",
"delete",
"=",
"in",
"of",
"...",
"*",
"/",
"%",
"++",
"--",
"+",
"-",
"~",
"!",
">>",
">>>",
"<<",
"<",
">",
"<=",
">=",
"instanceof",
"&",
"^",
"|",
"==",
"!=",
"===",
"!==",
]);
class PrettyFast {
/**
* @param {Object} options: Provides configurability of the pretty printing.
* @param {String} options.url: The URL string of the ugly JS code.
* @param {String} options.indent: The string to indent code by.
* @param {SourceMapGenerator} options.sourceMapGenerator: An optional sourceMapGenerator
* the mappings will be added to.
* @param {Boolean} options.prefixWithNewLine: When true, the pretty printed code will start
* with a line break
* @param {Integer} options.originalStartLine: The line the passed script starts at (1-based).
* This is used for inline scripts where we need to account for the lines
* before the script tag
* @param {Integer} options.originalStartColumn: The column the passed script starts at (1-based).
* This is used for inline scripts where we need to account for the position
* of the script tag within the line.
* @param {Integer} options.generatedStartLine: The line where the pretty printed script
* will start at (1-based). This is used for pretty printing HTML file,
* where we might have handle previous inline scripts that impact the
* position of this script.
*/
constructor(options = {}) {
// The level of indents deep we are.
this.#indentLevel = 0;
this.#indentChar = options.indent;
// We will handle mappings between ugly and pretty printed code in this SourceMapGenerator.
this.#sourceMapGenerator =
options.sourceMapGenerator ||
new sourceMap.SourceMapGenerator({
file: options.url,
});
this.#file = options.url;
this.#hasOriginalStartLine = "originalStartLine" in options;
this.#hasOriginalStartColumn = "originalStartColumn" in options;
this.#hasGeneratedStartLine = "generatedStartLine" in options;
this.#originalStartLine = options.originalStartLine;
this.#originalStartColumn = options.originalStartColumn;
this.#generatedStartLine = options.generatedStartLine;
this.#prefixWithNewLine = options.prefixWithNewLine;
}
/* options */
#indentChar;
#indentLevel;
#file;
#hasOriginalStartLine;
#hasOriginalStartColumn;
#hasGeneratedStartLine;
#originalStartLine;
#originalStartColumn;
#prefixWithNewLine;
#generatedStartLine;
#sourceMapGenerator;
/* internals */
// Whether or not we added a newline on after we added the previous token.
#addedNewline = false;
// Whether or not we added a space after we added the previous token.
#addedSpace = false;
#currentCode = "";
#currentLine = 1;
#currentColumn = 0;
// The tokens parsed by acorn.
#tokenQueue;
// The index of the current token in this.#tokenQueue.
#currentTokenIndex;
// The previous token we added to the pretty printed code.
#previousToken;
// Stack of token types/keywords that can affect whether we want to add a
// newline or a space. We can make that decision based on what token type is
// on the top of the stack. For example, a comma in a parameter list should
// be followed by a space, while a comma in an object literal should be
// followed by a newline.
//
// Strings that go on the stack:
//
// - "{"
// - "{\n"
// - "("
// - "(\n"
// - "["
// - "[\n"
// - "do"
// - "?"
// - "switch"
// - "case"
// - "default"
//
// The difference between "[" and "[\n" (as well as "{" and "{\n", and "(" and "(\n")
// is that "\n" is used when we are treating (curly) brackets/parens as line delimiters
// and should increment and decrement the indent level when we find them.
// "[" can represent either a property access (e.g. `x["hi"]`), or an empty array literal
// "{" only represents an empty object literals
// "(" can represent lots of different things (wrapping expression, if/loop condition, function call, …)
#stack = [];
/**
* @param {String} input: The ugly JS code we want to pretty print.
* @returns {Object}
* An object with the following properties:
* - code: The pretty printed code string.
* - map: A SourceMapGenerator instance.
*/
getPrettifiedCodeAndSourceMap(input) {
// Add the initial new line if needed
if (this.#prefixWithNewLine) {
this.#write("\n");
}
// Pass through acorn's tokenizer and append tokens and comments into a
// single queue to process. For example, the source file:
//
// foo
// // a
// // b
// bar
//
// After this process, tokenQueue has the following token stream:
//
// [ foo, '// a', '// b', bar]
this.#tokenQueue = this.#getTokens(input);
for (let i = 0, len = this.#tokenQueue.length; i < len; i++) {
this.#currentTokenIndex = i;
const token = this.#tokenQueue[i];
const nextToken = this.#tokenQueue[i + 1];
this.#handleToken(token, nextToken);
// Acorn's tokenizer re-uses tokens, so we have to copy the previous token on
// every iteration. We follow acorn's lead here, and reuse the previousToken
// object the same way that acorn reuses the token object. This allows us
// to avoid allocations and minimize GC pauses.
if (!this.#previousToken) {
this.#previousToken = { loc: { start: {}, end: {} } };
}
this.#previousToken.start = token.start;
this.#previousToken.end = token.end;
this.#previousToken.loc.start.line = token.loc.start.line;
this.#previousToken.loc.start.column = token.loc.start.column;
this.#previousToken.loc.end.line = token.loc.end.line;
this.#previousToken.loc.end.column = token.loc.end.column;
this.#previousToken.type = token.type;
this.#previousToken.value = token.value;
}
return { code: this.#currentCode, map: this.#sourceMapGenerator };
}
/**
* Write a pretty printed string to the prettified string and for tokens, add their
* mapping to the SourceMapGenerator.
*
* @param String str
* The string to be added to the result.
* @param Number line
* The line number the string came from in the ugly source.
* @param Number column
* The column number the string came from in the ugly source.
* @param Boolean isToken
* Set to true when writing tokens, so we can differentiate them from the
* whitespace we add.
*/
#write(str, line, column, isToken) {
this.#currentCode += str;
if (isToken) {
this.#sourceMapGenerator.addMapping({
source: this.#file,
// We need to swap original and generated locations, as the prettified text should
// be seen by the sourcemap service as the "original" one.
generated: {
// originalStartLine is 1-based, and here we just want to offset by a number of
// lines, so we need to decrement it
line: this.#hasOriginalStartLine
? line + (this.#originalStartLine - 1)
: line,
// We only need to adjust the column number if we're looking at the first line, to
// account for the html text before the opening