/* 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