123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874 |
- /* -*- indent-tabs-mode: nil; js-indent-level: 2; fill-column: 80 -*- */
- /*
- * Copyright 2013 Mozilla Foundation and contributors
- * Licensed under the New BSD license. See LICENSE.md or:
- * http://opensource.org/licenses/BSD-2-Clause
- */
- (function (root, factory) {
- "use strict";
- if (typeof define === "function" && define.amd) {
- define(factory);
- } else if (typeof exports === "object") {
- module.exports = factory();
- } else {
- root.prettyFast = factory();
- }
- }(this, function () {
- "use strict";
- var acorn = this.acorn || require("acorn/acorn");
- var sourceMap = this.sourceMap || require("source-map");
- var SourceNode = sourceMap.SourceNode;
- // 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.
- var PRE_ARRAY_LITERAL_TOKENS = {
- "typeof": true,
- "void": true,
- "delete": true,
- "case": true,
- "do": true,
- "=": true,
- "in": true,
- "{": true,
- "*": true,
- "/": true,
- "%": true,
- "else": true,
- ";": true,
- "++": true,
- "--": true,
- "+": true,
- "-": true,
- "~": true,
- "!": true,
- ":": true,
- "?": true,
- ">>": true,
- ">>>": true,
- "<<": true,
- "||": true,
- "&&": true,
- "<": true,
- ">": true,
- "<=": true,
- ">=": true,
- "instanceof": true,
- "&": true,
- "^": true,
- "|": true,
- "==": true,
- "!=": true,
- "===": true,
- "!==": true,
- ",": true,
- "}": true
- };
- /**
- * Determines if we think that the given token starts an array literal.
- *
- * @param Object token
- * The token we want to determine if it is an array literal.
- * @param Object lastToken
- * The last token we added to the pretty printed results.
- *
- * @returns Boolean
- * True if we believe it is an array literal, false otherwise.
- */
- function isArrayLiteral(token, lastToken) {
- if (token.type.label != "[") {
- return false;
- }
- if (!lastToken) {
- return true;
- }
- if (lastToken.type.isAssign) {
- return true;
- }
- return !!PRE_ARRAY_LITERAL_TOKENS[
- lastToken.type.keyword || lastToken.type.label
- ];
- }
- // If any of these tokens are followed by a token on a new line, we know that
- // ASI cannot happen.
- var PREVENT_ASI_AFTER_TOKENS = {
- // Binary operators
- "*": true,
- "/": true,
- "%": true,
- "+": true,
- "-": true,
- "<<": true,
- ">>": true,
- ">>>": true,
- "<": true,
- ">": true,
- "<=": true,
- ">=": true,
- "instanceof": true,
- "in": true,
- "==": true,
- "!=": true,
- "===": true,
- "!==": true,
- "&": true,
- "^": true,
- "|": true,
- "&&": true,
- "||": true,
- ",": true,
- ".": true,
- "=": true,
- "*=": true,
- "/=": true,
- "%=": true,
- "+=": true,
- "-=": true,
- "<<=": true,
- ">>=": true,
- ">>>=": true,
- "&=": true,
- "^=": true,
- "|=": true,
- // Unary operators
- "delete": true,
- "void": true,
- "typeof": true,
- "~": true,
- "!": true,
- "new": true,
- // Function calls and grouped expressions
- "(": true
- };
- // If any of these tokens are on a line after the token before it, we know
- // that ASI cannot happen.
- var PREVENT_ASI_BEFORE_TOKENS = {
- // Binary operators
- "*": true,
- "/": true,
- "%": true,
- "<<": true,
- ">>": true,
- ">>>": true,
- "<": true,
- ">": true,
- "<=": true,
- ">=": true,
- "instanceof": true,
- "in": true,
- "==": true,
- "!=": true,
- "===": true,
- "!==": true,
- "&": true,
- "^": true,
- "|": true,
- "&&": true,
- "||": true,
- ",": true,
- ".": true,
- "=": true,
- "*=": true,
- "/=": true,
- "%=": true,
- "+=": true,
- "-=": true,
- "<<=": true,
- ">>=": true,
- ">>>=": true,
- "&=": true,
- "^=": true,
- "|=": true,
- // Function calls
- "(": true
- };
- /**
- * Determines if Automatic Semicolon Insertion (ASI) occurs between these
- * tokens.
- *
- * @param Object token
- * The current token.
- * @param Object lastToken
- * The last token we added to the pretty printed results.
- *
- * @returns Boolean
- * True if we believe ASI occurs.
- */
- function isASI(token, lastToken) {
- if (!lastToken) {
- return false;
- }
- if (token.loc.start.line === lastToken.loc.start.line) {
- return false;
- }
- if (PREVENT_ASI_AFTER_TOKENS[
- lastToken.type.label || lastToken.type.keyword
- ]) {
- return false;
- }
- if (PREVENT_ASI_BEFORE_TOKENS[token.type.label || token.type.keyword]) {
- return false;
- }
- return true;
- }
- /**
- * Determine if we have encountered a getter or setter.
- *
- * @param Object token
- * The current token. If this is a getter or setter, it would be the
- * property name.
- * @param Object lastToken
- * The last token we added to the pretty printed results. If this is a
- * getter or setter, it would be the `get` or `set` keyword
- * respectively.
- * @param Array stack
- * The stack of open parens/curlies/brackets/etc.
- *
- * @returns Boolean
- * True if this is a getter or setter.
- */
- function isGetterOrSetter(token, lastToken, stack) {
- return stack[stack.length - 1] == "{"
- && lastToken
- && lastToken.type.label == "name"
- && (lastToken.value == "get" || lastToken.value == "set")
- && token.type.label == "name";
- }
- /**
- * Determine if we should add a newline after the given token.
- *
- * @param Object token
- * The token we are looking at.
- * @param Array stack
- * The stack of open parens/curlies/brackets/etc.
- *
- * @returns Boolean
- * True if we should add a newline.
- */
- function isLineDelimiter(token, stack) {
- if (token.isArrayLiteral) {
- return true;
- }
- var ttl = token.type.label;
- var top = stack[stack.length - 1];
- return ttl == ";" && top != "("
- || ttl == "{"
- || ttl == "," && top != "("
- || ttl == ":" && (top == "case" || top == "default");
- }
- /**
- * Append the necessary whitespace to the result after we have added the given
- * token.
- *
- * @param Object token
- * The token that was just added to the result.
- * @param Function write
- * The function to write to the pretty printed results.
- * @param Array stack
- * The stack of open parens/curlies/brackets/etc.
- *
- * @returns Boolean
- * Returns true if we added a newline to result, false in all other
- * cases.
- */
- function appendNewline(token, write, stack) {
- if (isLineDelimiter(token, stack)) {
- write("\n", token.loc.start.line, token.loc.start.column);
- return true;
- }
- return false;
- }
- /**
- * Determines if we need to add a space between the last token we added and
- * the token we are about to add.
- *
- * @param Object token
- * The token we are about to add to the pretty printed code.
- * @param Object lastToken
- * The last token added to the pretty printed code.
- */
- function needsSpaceAfter(token, lastToken) {
- if (lastToken) {
- if (lastToken.type.isLoop) {
- return true;
- }
- if (lastToken.type.isAssign) {
- return true;
- }
- if (lastToken.type.binop != null) {
- return true;
- }
- var ltt = lastToken.type.label;
- if (ltt == "?") {
- return true;
- }
- if (ltt == ":") {
- return true;
- }
- if (ltt == ",") {
- return true;
- }
- if (ltt == ";") {
- return true;
- }
- var ltk = lastToken.type.keyword;
- if (ltk != null) {
- if (ltk == "break" || ltk == "continue" || ltk == "return") {
- return token.type.label != ";";
- }
- if (ltk != "debugger"
- && ltk != "null"
- && ltk != "true"
- && ltk != "false"
- && ltk != "this"
- && ltk != "default") {
- return true;
- }
- }
- if (ltt == ")" && (token.type.label != ")"
- && token.type.label != "]"
- && token.type.label != ";"
- && token.type.label != ","
- && token.type.label != ".")) {
- return true;
- }
- }
- if (token.type.isAssign) {
- return true;
- }
- if (token.type.binop != null) {
- return true;
- }
- if (token.type.label == "?") {
- return true;
- }
- return false;
- }
- /**
- * Add the required whitespace before this token, whether that is a single
- * space, newline, and/or the indent on fresh lines.
- *
- * @param Object token
- * The token we are about to add to the pretty printed code.
- * @param Object lastToken
- * The last token we added to the pretty printed code.
- * @param Boolean addedNewline
- * Whether we added a newline after adding the last token to the pretty
- * printed code.
- * @param Function write
- * The function to write pretty printed code to the result SourceNode.
- * @param Object options
- * The options object.
- * @param Number indentLevel
- * The number of indents deep we are.
- * @param Array stack
- * The stack of open curlies, brackets, etc.
- */
- function prependWhiteSpace(token, lastToken, addedNewline, write, options,
- indentLevel, stack) {
- var ttk = token.type.keyword;
- var ttl = token.type.label;
- var newlineAdded = addedNewline;
- var ltt = lastToken ? lastToken.type.label : null;
- // Handle whitespace and newlines after "}" here instead of in
- // `isLineDelimiter` because it is only a line delimiter some of the
- // time. For example, we don't want to put "else if" on a new line after
- // the first if's block.
- if (lastToken && ltt == "}") {
- if (ttk == "while" && stack[stack.length - 1] == "do") {
- write(" ",
- lastToken.loc.start.line,
- lastToken.loc.start.column);
- } else if (ttk == "else" ||
- ttk == "catch" ||
- ttk == "finally") {
- write(" ",
- lastToken.loc.start.line,
- lastToken.loc.start.column);
- } else if (ttl != "(" &&
- ttl != ";" &&
- ttl != "," &&
- ttl != ")" &&
- ttl != ".") {
- write("\n",
- lastToken.loc.start.line,
- lastToken.loc.start.column);
- newlineAdded = true;
- }
- }
- if (isGetterOrSetter(token, lastToken, stack)) {
- write(" ",
- lastToken.loc.start.line,
- lastToken.loc.start.column);
- }
- if (ttl == ":" && stack[stack.length - 1] == "?") {
- write(" ",
- lastToken.loc.start.line,
- lastToken.loc.start.column);
- }
- if (lastToken && ltt != "}" && ttk == "else") {
- write(" ",
- lastToken.loc.start.line,
- lastToken.loc.start.column);
- }
- function ensureNewline() {
- if (!newlineAdded) {
- write("\n",
- lastToken.loc.start.line,
- lastToken.loc.start.column);
- newlineAdded = true;
- }
- }
- if (isASI(token, lastToken)) {
- ensureNewline();
- }
- if (decrementsIndent(ttl, stack)) {
- ensureNewline();
- }
- if (newlineAdded) {
- if (ttk == "case" || ttk == "default") {
- write(repeat(options.indent, indentLevel - 1),
- token.loc.start.line,
- token.loc.start.column);
- } else {
- write(repeat(options.indent, indentLevel),
- token.loc.start.line,
- token.loc.start.column);
- }
- } else if (needsSpaceAfter(token, lastToken)) {
- write(" ",
- lastToken.loc.start.line,
- lastToken.loc.start.column);
- }
- }
- /**
- * Repeat the `str` string `n` times.
- *
- * @param String str
- * The string to be repeated.
- * @param Number n
- * The number of times to repeat the string.
- *
- * @returns String
- * The repeated string.
- */
- function repeat(str, n) {
- var result = "";
- while (n > 0) {
- if (n & 1) {
- result += str;
- }
- n >>= 1;
- str += str;
- }
- return result;
- }
- /**
- * Make sure that we output the escaped character combination inside string
- * literals instead of various problematic characters.
- */
- var sanitize = (function () {
- var escapeCharacters = {
- // Backslash
- "\\": "\\\\",
- // Newlines
- "\n": "\\n",
- // Carriage return
- "\r": "\\r",
- // Tab
- "\t": "\\t",
- // Vertical tab
- "\v": "\\v",
- // Form feed
- "\f": "\\f",
- // Null character
- "\0": "\\0",
- // Single quotes
- "'": "\\'"
- };
- var regExpString = "("
- + Object.keys(escapeCharacters)
- .map(function (c) { return escapeCharacters[c]; })
- .join("|")
- + ")";
- var escapeCharactersRegExp = new RegExp(regExpString, "g");
- return function (str) {
- return str.replace(escapeCharactersRegExp, function (_, c) {
- return escapeCharacters[c];
- });
- };
- }());
- /**
- * Add the given token to the pretty printed results.
- *
- * @param Object token
- * The token to add.
- * @param Function write
- * The function to write pretty printed code to the result SourceNode.
- */
- function addToken(token, write) {
- if (token.type.label == "string") {
- write("'" + sanitize(token.value) + "'",
- token.loc.start.line,
- token.loc.start.column);
- } else if (token.type.label == "regexp") {
- write(String(token.value.value),
- token.loc.start.line,
- token.loc.start.column);
- } else {
- write(String(token.value != null ? token.value : token.type.label),
- token.loc.start.line,
- token.loc.start.column);
- }
- }
- /**
- * Returns true if the given token type belongs on the stack.
- */
- function belongsOnStack(token) {
- var ttl = token.type.label;
- var ttk = token.type.keyword;
- return ttl == "{"
- || ttl == "("
- || ttl == "["
- || ttl == "?"
- || ttk == "do"
- || ttk == "switch"
- || ttk == "case"
- || ttk == "default";
- }
- /**
- * Returns true if the given token should cause us to pop the stack.
- */
- function shouldStackPop(token, stack) {
- var ttl = token.type.label;
- var ttk = token.type.keyword;
- var top = stack[stack.length - 1];
- return ttl == "]"
- || ttl == ")"
- || ttl == "}"
- || (ttl == ":" && (top == "case" || top == "default" || top == "?"))
- || (ttk == "while" && top == "do");
- }
- /**
- * Returns true if the given token type should cause us to decrement the
- * indent level.
- */
- function decrementsIndent(tokenType, stack) {
- return tokenType == "}"
- || (tokenType == "]" && stack[stack.length - 1] == "[\n");
- }
- /**
- * Returns true if the given token should cause us to increment the indent
- * level.
- */
- function incrementsIndent(token) {
- return token.type.label == "{"
- || token.isArrayLiteral
- || token.type.keyword == "switch";
- }
- /**
- * Add a comment to the pretty printed code.
- *
- * @param Function write
- * The function to write pretty printed code to the result SourceNode.
- * @param Number indentLevel
- * The number of indents deep we are.
- * @param Object options
- * The options object.
- * @param Boolean block
- * True if the comment is a multiline block style comment.
- * @param String text
- * The text of the comment.
- * @param Number line
- * The line number to comment appeared on.
- * @param Number column
- * The column number the comment appeared on.
- */
- function addComment(write, indentLevel, options, block, text, line, column) {
- var indentString = repeat(options.indent, indentLevel);
- write(indentString, line, column);
- if (block) {
- write("/*");
- write(text
- .split(new RegExp("/\n" + indentString + "/", "g"))
- .join("\n" + indentString));
- write("*/");
- } else {
- write("//");
- write(text);
- }
- write("\n");
- }
- /**
- * The main function.
- *
- * @param String input
- * The ugly JS code we want to pretty print.
- * @param Object options
- * The options object. Provides configurability of the pretty
- * printing. Properties:
- * - url: The URL string of the ugly JS code.
- * - indent: The string to indent code by.
- *
- * @returns Object
- * An object with the following properties:
- * - code: The pretty printed code string.
- * - map: A SourceMapGenerator instance.
- */
- return function prettyFast(input, options) {
- // The level of indents deep we are.
- var indentLevel = 0;
- // We will accumulate the pretty printed code in this SourceNode.
- var result = new SourceNode();
- /**
- * Write a pretty printed string to the result SourceNode.
- *
- * We buffer our writes so that we only create one mapping for each line in
- * the source map. This enhances performance by avoiding extraneous mapping
- * serialization, and flattening the tree that
- * `SourceNode#toStringWithSourceMap` will have to recursively walk. When
- * timing how long it takes to pretty print jQuery, this optimization
- * brought the time down from ~390 ms to ~190ms!
- *
- * @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.
- */
- var write = (function () {
- var buffer = [];
- var bufferLine = -1;
- var bufferColumn = -1;
- return function write(str, line, column) {
- if (line != null && bufferLine === -1) {
- bufferLine = line;
- }
- if (column != null && bufferColumn === -1) {
- bufferColumn = column;
- }
- buffer.push(str);
- if (str == "\n") {
- var lineStr = "";
- for (var i = 0, len = buffer.length; i < len; i++) {
- lineStr += buffer[i];
- }
- result.add(new SourceNode(bufferLine, bufferColumn, options.url,
- lineStr));
- buffer.splice(0, buffer.length);
- bufferLine = -1;
- bufferColumn = -1;
- }
- };
- }());
- // Whether or not we added a newline on after we added the last token.
- var addedNewline = false;
- // The current token we will be adding to the pretty printed code.
- var token;
- // Shorthand for token.type.label, so we don't have to repeatedly access
- // properties.
- var ttl;
- // Shorthand for token.type.keyword, so we don't have to repeatedly access
- // properties.
- var ttk;
- // The last token we added to the pretty printed code.
- var lastToken;
- // 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"
- // - "do"
- // - "?"
- // - "switch"
- // - "case"
- // - "default"
- //
- // The difference between "[" and "[\n" is that "[\n" is used when we are
- // treating "[" and "]" tokens as line delimiters and should increment and
- // decrement the indent level when we find them.
- var stack = [];
- // 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]
- var tokenQueue = [];
- var tokens = acorn.tokenizer(input, {
- locations: true,
- sourceFile: options.url,
- onComment: function (block, text, start, end, startLoc, endLoc) {
- tokenQueue.push({
- type: {},
- comment: true,
- block: block,
- text: text,
- loc: { start: startLoc, end: endLoc }
- });
- }
- });
- for (;;) {
- token = tokens.getToken();
- tokenQueue.push(token);
- if (token.type.label == "eof") {
- break;
- }
- }
- for (var i = 0; i < tokenQueue.length; i++) {
- token = tokenQueue[i];
- if (token.comment) {
- var commentIndentLevel = indentLevel;
- if (lastToken && (lastToken.loc.end.line == token.loc.start.line)) {
- commentIndentLevel = 0;
- write(" ");
- }
- addComment(write, commentIndentLevel, options, token.block, token.text,
- token.loc.start.line, token.loc.start.column);
- addedNewline = true;
- continue;
- }
- ttk = token.type.keyword;
- ttl = token.type.label;
- if (ttl == "eof") {
- if (!addedNewline) {
- write("\n");
- }
- break;
- }
- token.isArrayLiteral = isArrayLiteral(token, lastToken);
- if (belongsOnStack(token)) {
- if (token.isArrayLiteral) {
- stack.push("[\n");
- } else {
- stack.push(ttl || ttk);
- }
- }
- if (decrementsIndent(ttl, stack)) {
- indentLevel--;
- if (ttl == "}"
- && stack.length > 1
- && stack[stack.length - 2] == "switch") {
- indentLevel--;
- }
- }
- prependWhiteSpace(token, lastToken, addedNewline, write, options,
- indentLevel, stack);
- addToken(token, write);
- // If the next token is going to be a comment starting on the same line,
- // then no need to add one here
- var nextToken = tokenQueue[i + 1];
- if (!nextToken || !nextToken.comment || token.loc.end.line != nextToken.loc.start.line) {
- addedNewline = appendNewline(token, write, stack);
- }
- if (shouldStackPop(token, stack)) {
- stack.pop();
- if (token == "}" && stack.length
- && stack[stack.length - 1] == "switch") {
- stack.pop();
- }
- }
- if (incrementsIndent(token)) {
- indentLevel++;
- }
- // Acorn's tokenizer re-uses tokens, so we have to copy the last token on
- // every iteration. We follow acorn's lead here, and reuse the lastToken
- // object the same way that acorn reuses the token object. This allows us
- // to avoid allocations and minimize GC pauses.
- if (!lastToken) {
- lastToken = { loc: { start: {}, end: {} } };
- }
- lastToken.start = token.start;
- lastToken.end = token.end;
- lastToken.loc.start.line = token.loc.start.line;
- lastToken.loc.start.column = token.loc.start.column;
- lastToken.loc.end.line = token.loc.end.line;
- lastToken.loc.end.column = token.loc.end.column;
- lastToken.type = token.type;
- lastToken.value = token.value;
- lastToken.isArrayLiteral = token.isArrayLiteral;
- }
- return result.toStringWithSourceMap({ file: options.url });
- };
- }.bind(this)));
|