Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | /* |
michael@0 | 2 | * Copyright 2013 Mozilla Foundation and contributors |
michael@0 | 3 | * Licensed under the New BSD license. See LICENSE.md or: |
michael@0 | 4 | * http://opensource.org/licenses/BSD-2-Clause |
michael@0 | 5 | */ |
michael@0 | 6 | (function (root, factory) { |
michael@0 | 7 | if (typeof define === 'function' && define.amd) { |
michael@0 | 8 | define(factory); |
michael@0 | 9 | } else if (typeof exports === 'object') { |
michael@0 | 10 | module.exports = factory(); |
michael@0 | 11 | } else { |
michael@0 | 12 | root.prettyFast = factory(); |
michael@0 | 13 | } |
michael@0 | 14 | }(this, function () { |
michael@0 | 15 | "use strict"; |
michael@0 | 16 | |
michael@0 | 17 | var acorn = this.acorn || require("acorn/acorn"); |
michael@0 | 18 | var sourceMap = this.sourceMap || require("source-map"); |
michael@0 | 19 | var SourceNode = sourceMap.SourceNode; |
michael@0 | 20 | |
michael@0 | 21 | // If any of these tokens are seen before a "[" token, we know that "[" token |
michael@0 | 22 | // is the start of an array literal, rather than a property access. |
michael@0 | 23 | // |
michael@0 | 24 | // The only exception is "}", which would need to be disambiguated by |
michael@0 | 25 | // parsing. The majority of the time, an open bracket following a closing |
michael@0 | 26 | // curly is going to be an array literal, so we brush the complication under |
michael@0 | 27 | // the rug, and handle the ambiguity by always assuming that it will be an |
michael@0 | 28 | // array literal. |
michael@0 | 29 | var PRE_ARRAY_LITERAL_TOKENS = { |
michael@0 | 30 | "typeof": true, |
michael@0 | 31 | "void": true, |
michael@0 | 32 | "delete": true, |
michael@0 | 33 | "case": true, |
michael@0 | 34 | "do": true, |
michael@0 | 35 | "=": true, |
michael@0 | 36 | "in": true, |
michael@0 | 37 | "{": true, |
michael@0 | 38 | "*": true, |
michael@0 | 39 | "/": true, |
michael@0 | 40 | "%": true, |
michael@0 | 41 | "else": true, |
michael@0 | 42 | ";": true, |
michael@0 | 43 | "++": true, |
michael@0 | 44 | "--": true, |
michael@0 | 45 | "+": true, |
michael@0 | 46 | "-": true, |
michael@0 | 47 | "~": true, |
michael@0 | 48 | "!": true, |
michael@0 | 49 | ":": true, |
michael@0 | 50 | "?": true, |
michael@0 | 51 | ">>": true, |
michael@0 | 52 | ">>>": true, |
michael@0 | 53 | "<<": true, |
michael@0 | 54 | "||": true, |
michael@0 | 55 | "&&": true, |
michael@0 | 56 | "<": true, |
michael@0 | 57 | ">": true, |
michael@0 | 58 | "<=": true, |
michael@0 | 59 | ">=": true, |
michael@0 | 60 | "instanceof": true, |
michael@0 | 61 | "&": true, |
michael@0 | 62 | "^": true, |
michael@0 | 63 | "|": true, |
michael@0 | 64 | "==": true, |
michael@0 | 65 | "!=": true, |
michael@0 | 66 | "===": true, |
michael@0 | 67 | "!==": true, |
michael@0 | 68 | ",": true, |
michael@0 | 69 | |
michael@0 | 70 | "}": true |
michael@0 | 71 | }; |
michael@0 | 72 | |
michael@0 | 73 | /** |
michael@0 | 74 | * Determines if we think that the given token starts an array literal. |
michael@0 | 75 | * |
michael@0 | 76 | * @param Object token |
michael@0 | 77 | * The token we want to determine if it is an array literal. |
michael@0 | 78 | * @param Object lastToken |
michael@0 | 79 | * The last token we added to the pretty printed results. |
michael@0 | 80 | * |
michael@0 | 81 | * @returns Boolean |
michael@0 | 82 | * True if we believe it is an array literal, false otherwise. |
michael@0 | 83 | */ |
michael@0 | 84 | function isArrayLiteral(token, lastToken) { |
michael@0 | 85 | if (token.type.type != "[") { |
michael@0 | 86 | return false; |
michael@0 | 87 | } |
michael@0 | 88 | if (!lastToken) { |
michael@0 | 89 | return true; |
michael@0 | 90 | } |
michael@0 | 91 | if (lastToken.type.isAssign) { |
michael@0 | 92 | return true; |
michael@0 | 93 | } |
michael@0 | 94 | return !!PRE_ARRAY_LITERAL_TOKENS[lastToken.type.keyword || lastToken.type.type]; |
michael@0 | 95 | } |
michael@0 | 96 | |
michael@0 | 97 | // If any of these tokens are followed by a token on a new line, we know that |
michael@0 | 98 | // ASI cannot happen. |
michael@0 | 99 | var PREVENT_ASI_AFTER_TOKENS = { |
michael@0 | 100 | // Binary operators |
michael@0 | 101 | "*": true, |
michael@0 | 102 | "/": true, |
michael@0 | 103 | "%": true, |
michael@0 | 104 | "+": true, |
michael@0 | 105 | "-": true, |
michael@0 | 106 | "<<": true, |
michael@0 | 107 | ">>": true, |
michael@0 | 108 | ">>>": true, |
michael@0 | 109 | "<": true, |
michael@0 | 110 | ">": true, |
michael@0 | 111 | "<=": true, |
michael@0 | 112 | ">=": true, |
michael@0 | 113 | "instanceof": true, |
michael@0 | 114 | "in": true, |
michael@0 | 115 | "==": true, |
michael@0 | 116 | "!=": true, |
michael@0 | 117 | "===": true, |
michael@0 | 118 | "!==": true, |
michael@0 | 119 | "&": true, |
michael@0 | 120 | "^": true, |
michael@0 | 121 | "|": true, |
michael@0 | 122 | "&&": true, |
michael@0 | 123 | "||": true, |
michael@0 | 124 | ",": true, |
michael@0 | 125 | ".": true, |
michael@0 | 126 | "=": true, |
michael@0 | 127 | "*=": true, |
michael@0 | 128 | "/=": true, |
michael@0 | 129 | "%=": true, |
michael@0 | 130 | "+=": true, |
michael@0 | 131 | "-=": true, |
michael@0 | 132 | "<<=": true, |
michael@0 | 133 | ">>=": true, |
michael@0 | 134 | ">>>=": true, |
michael@0 | 135 | "&=": true, |
michael@0 | 136 | "^=": true, |
michael@0 | 137 | "|=": true, |
michael@0 | 138 | // Unary operators |
michael@0 | 139 | "delete": true, |
michael@0 | 140 | "void": true, |
michael@0 | 141 | "typeof": true, |
michael@0 | 142 | "~": true, |
michael@0 | 143 | "!": true, |
michael@0 | 144 | "new": true, |
michael@0 | 145 | // Function calls and grouped expressions |
michael@0 | 146 | "(": true |
michael@0 | 147 | }; |
michael@0 | 148 | |
michael@0 | 149 | // If any of these tokens are on a line after the token before it, we know |
michael@0 | 150 | // that ASI cannot happen. |
michael@0 | 151 | var PREVENT_ASI_BEFORE_TOKENS = { |
michael@0 | 152 | // Binary operators |
michael@0 | 153 | "*": true, |
michael@0 | 154 | "/": true, |
michael@0 | 155 | "%": true, |
michael@0 | 156 | "<<": true, |
michael@0 | 157 | ">>": true, |
michael@0 | 158 | ">>>": true, |
michael@0 | 159 | "<": true, |
michael@0 | 160 | ">": true, |
michael@0 | 161 | "<=": true, |
michael@0 | 162 | ">=": true, |
michael@0 | 163 | "instanceof": true, |
michael@0 | 164 | "in": true, |
michael@0 | 165 | "==": true, |
michael@0 | 166 | "!=": true, |
michael@0 | 167 | "===": true, |
michael@0 | 168 | "!==": true, |
michael@0 | 169 | "&": true, |
michael@0 | 170 | "^": true, |
michael@0 | 171 | "|": true, |
michael@0 | 172 | "&&": true, |
michael@0 | 173 | "||": true, |
michael@0 | 174 | ",": true, |
michael@0 | 175 | ".": true, |
michael@0 | 176 | "=": true, |
michael@0 | 177 | "*=": true, |
michael@0 | 178 | "/=": true, |
michael@0 | 179 | "%=": true, |
michael@0 | 180 | "+=": true, |
michael@0 | 181 | "-=": true, |
michael@0 | 182 | "<<=": true, |
michael@0 | 183 | ">>=": true, |
michael@0 | 184 | ">>>=": true, |
michael@0 | 185 | "&=": true, |
michael@0 | 186 | "^=": true, |
michael@0 | 187 | "|=": true, |
michael@0 | 188 | // Function calls |
michael@0 | 189 | "(": true |
michael@0 | 190 | }; |
michael@0 | 191 | |
michael@0 | 192 | /** |
michael@0 | 193 | * Determines if Automatic Semicolon Insertion (ASI) occurs between these |
michael@0 | 194 | * tokens. |
michael@0 | 195 | * |
michael@0 | 196 | * @param Object token |
michael@0 | 197 | * The current token. |
michael@0 | 198 | * @param Object lastToken |
michael@0 | 199 | * The last token we added to the pretty printed results. |
michael@0 | 200 | * |
michael@0 | 201 | * @returns Boolean |
michael@0 | 202 | * True if we believe ASI occurs. |
michael@0 | 203 | */ |
michael@0 | 204 | function isASI(token, lastToken) { |
michael@0 | 205 | if (!lastToken) { |
michael@0 | 206 | return false; |
michael@0 | 207 | } |
michael@0 | 208 | if (token.startLoc.line === lastToken.startLoc.line) { |
michael@0 | 209 | return false; |
michael@0 | 210 | } |
michael@0 | 211 | if (PREVENT_ASI_AFTER_TOKENS[lastToken.type.type || lastToken.type.keyword]) { |
michael@0 | 212 | return false; |
michael@0 | 213 | } |
michael@0 | 214 | if (PREVENT_ASI_BEFORE_TOKENS[token.type.type || token.type.keyword]) { |
michael@0 | 215 | return false; |
michael@0 | 216 | } |
michael@0 | 217 | return true; |
michael@0 | 218 | } |
michael@0 | 219 | |
michael@0 | 220 | /** |
michael@0 | 221 | * Determine if we have encountered a getter or setter. |
michael@0 | 222 | * |
michael@0 | 223 | * @param Object token |
michael@0 | 224 | * The current token. If this is a getter or setter, it would be the |
michael@0 | 225 | * property name. |
michael@0 | 226 | * @param Object lastToken |
michael@0 | 227 | * The last token we added to the pretty printed results. If this is a |
michael@0 | 228 | * getter or setter, it would be the `get` or `set` keyword |
michael@0 | 229 | * respectively. |
michael@0 | 230 | * @param Array stack |
michael@0 | 231 | * The stack of open parens/curlies/brackets/etc. |
michael@0 | 232 | * |
michael@0 | 233 | * @returns Boolean |
michael@0 | 234 | * True if this is a getter or setter. |
michael@0 | 235 | */ |
michael@0 | 236 | function isGetterOrSetter(token, lastToken, stack) { |
michael@0 | 237 | return stack[stack.length - 1] == "{" |
michael@0 | 238 | && lastToken |
michael@0 | 239 | && lastToken.type.type == "name" |
michael@0 | 240 | && (lastToken.value == "get" || lastToken.value == "set") |
michael@0 | 241 | && token.type.type == "name"; |
michael@0 | 242 | } |
michael@0 | 243 | |
michael@0 | 244 | /** |
michael@0 | 245 | * Determine if we should add a newline after the given token. |
michael@0 | 246 | * |
michael@0 | 247 | * @param Object token |
michael@0 | 248 | * The token we are looking at. |
michael@0 | 249 | * @param Array stack |
michael@0 | 250 | * The stack of open parens/curlies/brackets/etc. |
michael@0 | 251 | * |
michael@0 | 252 | * @returns Boolean |
michael@0 | 253 | * True if we should add a newline. |
michael@0 | 254 | */ |
michael@0 | 255 | function isLineDelimiter(token, stack) { |
michael@0 | 256 | if (token.isArrayLiteral) { |
michael@0 | 257 | return true; |
michael@0 | 258 | } |
michael@0 | 259 | var ttt = token.type.type; |
michael@0 | 260 | var top = stack[stack.length - 1]; |
michael@0 | 261 | return ttt == ";" && top != "(" |
michael@0 | 262 | || ttt == "{" |
michael@0 | 263 | || ttt == "," && top != "(" |
michael@0 | 264 | || ttt == ":" && (top == "case" || top == "default"); |
michael@0 | 265 | } |
michael@0 | 266 | |
michael@0 | 267 | /** |
michael@0 | 268 | * Append the necessary whitespace to the result after we have added the given |
michael@0 | 269 | * token. |
michael@0 | 270 | * |
michael@0 | 271 | * @param Object token |
michael@0 | 272 | * The token that was just added to the result. |
michael@0 | 273 | * @param Function write |
michael@0 | 274 | * The function to write to the pretty printed results. |
michael@0 | 275 | * @param Array stack |
michael@0 | 276 | * The stack of open parens/curlies/brackets/etc. |
michael@0 | 277 | * |
michael@0 | 278 | * @returns Boolean |
michael@0 | 279 | * Returns true if we added a newline to result, false in all other |
michael@0 | 280 | * cases. |
michael@0 | 281 | */ |
michael@0 | 282 | function appendNewline(token, write, stack) { |
michael@0 | 283 | if (isLineDelimiter(token, stack)) { |
michael@0 | 284 | write("\n", token.startLoc.line, token.startLoc.column); |
michael@0 | 285 | return true; |
michael@0 | 286 | } |
michael@0 | 287 | return false; |
michael@0 | 288 | } |
michael@0 | 289 | |
michael@0 | 290 | /** |
michael@0 | 291 | * Determines if we need to add a space between the last token we added and |
michael@0 | 292 | * the token we are about to add. |
michael@0 | 293 | * |
michael@0 | 294 | * @param Object token |
michael@0 | 295 | * The token we are about to add to the pretty printed code. |
michael@0 | 296 | * @param Object lastToken |
michael@0 | 297 | * The last token added to the pretty printed code. |
michael@0 | 298 | */ |
michael@0 | 299 | function needsSpaceAfter(token, lastToken) { |
michael@0 | 300 | if (lastToken) { |
michael@0 | 301 | if (lastToken.type.isLoop) { |
michael@0 | 302 | return true; |
michael@0 | 303 | } |
michael@0 | 304 | if (lastToken.type.isAssign) { |
michael@0 | 305 | return true; |
michael@0 | 306 | } |
michael@0 | 307 | if (lastToken.type.binop != null) { |
michael@0 | 308 | return true; |
michael@0 | 309 | } |
michael@0 | 310 | |
michael@0 | 311 | var ltt = lastToken.type.type; |
michael@0 | 312 | if (ltt == "?") { |
michael@0 | 313 | return true; |
michael@0 | 314 | } |
michael@0 | 315 | if (ltt == ":") { |
michael@0 | 316 | return true; |
michael@0 | 317 | } |
michael@0 | 318 | if (ltt == ",") { |
michael@0 | 319 | return true; |
michael@0 | 320 | } |
michael@0 | 321 | if (ltt == ";") { |
michael@0 | 322 | return true; |
michael@0 | 323 | } |
michael@0 | 324 | |
michael@0 | 325 | var ltk = lastToken.type.keyword; |
michael@0 | 326 | if (ltk != null) { |
michael@0 | 327 | if (ltk == "break" || ltk == "continue") { |
michael@0 | 328 | return token.type.type != ";"; |
michael@0 | 329 | } |
michael@0 | 330 | if (ltk != "debugger" |
michael@0 | 331 | && ltk != "null" |
michael@0 | 332 | && ltk != "true" |
michael@0 | 333 | && ltk != "false" |
michael@0 | 334 | && ltk != "this" |
michael@0 | 335 | && ltk != "default") { |
michael@0 | 336 | return true; |
michael@0 | 337 | } |
michael@0 | 338 | } |
michael@0 | 339 | |
michael@0 | 340 | if (ltt == ")" && (token.type.type != ")" |
michael@0 | 341 | && token.type.type != "]" |
michael@0 | 342 | && token.type.type != ";" |
michael@0 | 343 | && token.type.type != ",")) { |
michael@0 | 344 | return true; |
michael@0 | 345 | } |
michael@0 | 346 | } |
michael@0 | 347 | |
michael@0 | 348 | if (token.type.isAssign) { |
michael@0 | 349 | return true; |
michael@0 | 350 | } |
michael@0 | 351 | if (token.type.binop != null) { |
michael@0 | 352 | return true; |
michael@0 | 353 | } |
michael@0 | 354 | if (token.type.type == "?") { |
michael@0 | 355 | return true; |
michael@0 | 356 | } |
michael@0 | 357 | |
michael@0 | 358 | return false; |
michael@0 | 359 | } |
michael@0 | 360 | |
michael@0 | 361 | /** |
michael@0 | 362 | * Add the required whitespace before this token, whether that is a single |
michael@0 | 363 | * space, newline, and/or the indent on fresh lines. |
michael@0 | 364 | * |
michael@0 | 365 | * @param Object token |
michael@0 | 366 | * The token we are about to add to the pretty printed code. |
michael@0 | 367 | * @param Object lastToken |
michael@0 | 368 | * The last token we added to the pretty printed code. |
michael@0 | 369 | * @param Boolean addedNewline |
michael@0 | 370 | * Whether we added a newline after adding the last token to the pretty |
michael@0 | 371 | * printed code. |
michael@0 | 372 | * @param Function write |
michael@0 | 373 | * The function to write pretty printed code to the result SourceNode. |
michael@0 | 374 | * @param Object options |
michael@0 | 375 | * The options object. |
michael@0 | 376 | * @param Number indentLevel |
michael@0 | 377 | * The number of indents deep we are. |
michael@0 | 378 | * @param Array stack |
michael@0 | 379 | * The stack of open curlies, brackets, etc. |
michael@0 | 380 | */ |
michael@0 | 381 | function prependWhiteSpace(token, lastToken, addedNewline, write, options, |
michael@0 | 382 | indentLevel, stack) { |
michael@0 | 383 | var ttk = token.type.keyword; |
michael@0 | 384 | var ttt = token.type.type; |
michael@0 | 385 | var newlineAdded = addedNewline; |
michael@0 | 386 | var ltt = lastToken ? lastToken.type.type : null; |
michael@0 | 387 | |
michael@0 | 388 | // Handle whitespace and newlines after "}" here instead of in |
michael@0 | 389 | // `isLineDelimiter` because it is only a line delimiter some of the |
michael@0 | 390 | // time. For example, we don't want to put "else if" on a new line after |
michael@0 | 391 | // the first if's block. |
michael@0 | 392 | if (lastToken && ltt == "}") { |
michael@0 | 393 | if (ttk == "while" && stack[stack.length - 1] == "do") { |
michael@0 | 394 | write(" ", |
michael@0 | 395 | lastToken.startLoc.line, |
michael@0 | 396 | lastToken.startLoc.column); |
michael@0 | 397 | } else if (ttk == "else" || |
michael@0 | 398 | ttk == "catch" || |
michael@0 | 399 | ttk == "finally") { |
michael@0 | 400 | write(" ", |
michael@0 | 401 | lastToken.startLoc.line, |
michael@0 | 402 | lastToken.startLoc.column); |
michael@0 | 403 | } else if (ttt != "(" && |
michael@0 | 404 | ttt != ";" && |
michael@0 | 405 | ttt != "," && |
michael@0 | 406 | ttt != ")" && |
michael@0 | 407 | ttt != ".") { |
michael@0 | 408 | write("\n", |
michael@0 | 409 | lastToken.startLoc.line, |
michael@0 | 410 | lastToken.startLoc.column); |
michael@0 | 411 | newlineAdded = true; |
michael@0 | 412 | } |
michael@0 | 413 | } |
michael@0 | 414 | |
michael@0 | 415 | if (isGetterOrSetter(token, lastToken, stack)) { |
michael@0 | 416 | write(" ", |
michael@0 | 417 | lastToken.startLoc.line, |
michael@0 | 418 | lastToken.startLoc.column); |
michael@0 | 419 | } |
michael@0 | 420 | |
michael@0 | 421 | if (ttt == ":" && stack[stack.length - 1] == "?") { |
michael@0 | 422 | write(" ", |
michael@0 | 423 | lastToken.startLoc.line, |
michael@0 | 424 | lastToken.startLoc.column); |
michael@0 | 425 | } |
michael@0 | 426 | |
michael@0 | 427 | if (lastToken && ltt != "}" && ttk == "else") { |
michael@0 | 428 | write(" ", |
michael@0 | 429 | lastToken.startLoc.line, |
michael@0 | 430 | lastToken.startLoc.column); |
michael@0 | 431 | } |
michael@0 | 432 | |
michael@0 | 433 | function ensureNewline() { |
michael@0 | 434 | if (!newlineAdded) { |
michael@0 | 435 | write("\n", |
michael@0 | 436 | lastToken.startLoc.line, |
michael@0 | 437 | lastToken.startLoc.column); |
michael@0 | 438 | newlineAdded = true; |
michael@0 | 439 | } |
michael@0 | 440 | } |
michael@0 | 441 | |
michael@0 | 442 | if (isASI(token, lastToken)) { |
michael@0 | 443 | ensureNewline(); |
michael@0 | 444 | } |
michael@0 | 445 | |
michael@0 | 446 | if (decrementsIndent(ttt, stack)) { |
michael@0 | 447 | ensureNewline(); |
michael@0 | 448 | } |
michael@0 | 449 | |
michael@0 | 450 | if (newlineAdded) { |
michael@0 | 451 | if (ttk == "case" || ttk == "default") { |
michael@0 | 452 | write(repeat(options.indent, indentLevel - 1), |
michael@0 | 453 | token.startLoc.line, |
michael@0 | 454 | token.startLoc.column); |
michael@0 | 455 | } else { |
michael@0 | 456 | write(repeat(options.indent, indentLevel), |
michael@0 | 457 | token.startLoc.line, |
michael@0 | 458 | token.startLoc.column); |
michael@0 | 459 | } |
michael@0 | 460 | } else if (needsSpaceAfter(token, lastToken)) { |
michael@0 | 461 | write(" ", |
michael@0 | 462 | lastToken.startLoc.line, |
michael@0 | 463 | lastToken.startLoc.column); |
michael@0 | 464 | } |
michael@0 | 465 | } |
michael@0 | 466 | |
michael@0 | 467 | /** |
michael@0 | 468 | * Repeat the `str` string `n` times. |
michael@0 | 469 | * |
michael@0 | 470 | * @param String str |
michael@0 | 471 | * The string to be repeated. |
michael@0 | 472 | * @param Number n |
michael@0 | 473 | * The number of times to repeat the string. |
michael@0 | 474 | * |
michael@0 | 475 | * @returns String |
michael@0 | 476 | * The repeated string. |
michael@0 | 477 | */ |
michael@0 | 478 | function repeat(str, n) { |
michael@0 | 479 | var result = ""; |
michael@0 | 480 | while (n > 0) { |
michael@0 | 481 | if (n & 1) { |
michael@0 | 482 | result += str; |
michael@0 | 483 | } |
michael@0 | 484 | n >>= 1; |
michael@0 | 485 | str += str; |
michael@0 | 486 | } |
michael@0 | 487 | return result; |
michael@0 | 488 | } |
michael@0 | 489 | |
michael@0 | 490 | /** |
michael@0 | 491 | * Make sure that we output the escaped character combination inside string literals |
michael@0 | 492 | * instead of various problematic characters. |
michael@0 | 493 | */ |
michael@0 | 494 | var sanitize = (function () { |
michael@0 | 495 | var escapeCharacters = { |
michael@0 | 496 | // Backslash |
michael@0 | 497 | "\\": "\\\\", |
michael@0 | 498 | // Newlines |
michael@0 | 499 | "\n": "\\n", |
michael@0 | 500 | // Carriage return |
michael@0 | 501 | "\r": "\\r", |
michael@0 | 502 | // Tab |
michael@0 | 503 | "\t": "\\t", |
michael@0 | 504 | // Vertical tab |
michael@0 | 505 | "\v": "\\v", |
michael@0 | 506 | // Form feed |
michael@0 | 507 | "\f": "\\f", |
michael@0 | 508 | // Null character |
michael@0 | 509 | "\0": "\\0", |
michael@0 | 510 | // Single quotes |
michael@0 | 511 | "'": "\\'" |
michael@0 | 512 | }; |
michael@0 | 513 | |
michael@0 | 514 | var regExpString = "(" |
michael@0 | 515 | + Object.keys(escapeCharacters) |
michael@0 | 516 | .map(function (c) { return escapeCharacters[c]; }) |
michael@0 | 517 | .join("|") |
michael@0 | 518 | + ")"; |
michael@0 | 519 | var escapeCharactersRegExp = new RegExp(regExpString, "g"); |
michael@0 | 520 | |
michael@0 | 521 | return function(str) { |
michael@0 | 522 | return str.replace(escapeCharactersRegExp, function (_, c) { |
michael@0 | 523 | return escapeCharacters[c]; |
michael@0 | 524 | }); |
michael@0 | 525 | } |
michael@0 | 526 | }()); |
michael@0 | 527 | /** |
michael@0 | 528 | * Add the given token to the pretty printed results. |
michael@0 | 529 | * |
michael@0 | 530 | * @param Object token |
michael@0 | 531 | * The token to add. |
michael@0 | 532 | * @param Function write |
michael@0 | 533 | * The function to write pretty printed code to the result SourceNode. |
michael@0 | 534 | * @param Object options |
michael@0 | 535 | * The options object. |
michael@0 | 536 | */ |
michael@0 | 537 | function addToken(token, write, options) { |
michael@0 | 538 | if (token.type.type == "string") { |
michael@0 | 539 | write("'" + sanitize(token.value) + "'", |
michael@0 | 540 | token.startLoc.line, |
michael@0 | 541 | token.startLoc.column); |
michael@0 | 542 | } else { |
michael@0 | 543 | write(String(token.value != null ? token.value : token.type.type), |
michael@0 | 544 | token.startLoc.line, |
michael@0 | 545 | token.startLoc.column); |
michael@0 | 546 | } |
michael@0 | 547 | } |
michael@0 | 548 | |
michael@0 | 549 | /** |
michael@0 | 550 | * Returns true if the given token type belongs on the stack. |
michael@0 | 551 | */ |
michael@0 | 552 | function belongsOnStack(token) { |
michael@0 | 553 | var ttt = token.type.type; |
michael@0 | 554 | var ttk = token.type.keyword; |
michael@0 | 555 | return ttt == "{" |
michael@0 | 556 | || ttt == "(" |
michael@0 | 557 | || ttt == "[" |
michael@0 | 558 | || ttt == "?" |
michael@0 | 559 | || ttk == "do" |
michael@0 | 560 | || ttk == "case" |
michael@0 | 561 | || ttk == "default"; |
michael@0 | 562 | } |
michael@0 | 563 | |
michael@0 | 564 | /** |
michael@0 | 565 | * Returns true if the given token should cause us to pop the stack. |
michael@0 | 566 | */ |
michael@0 | 567 | function shouldStackPop(token, stack) { |
michael@0 | 568 | var ttt = token.type.type; |
michael@0 | 569 | var ttk = token.type.keyword; |
michael@0 | 570 | var top = stack[stack.length - 1]; |
michael@0 | 571 | return ttt == "]" |
michael@0 | 572 | || ttt == ")" |
michael@0 | 573 | || ttt == "}" |
michael@0 | 574 | || (ttt == ":" && (top == "case" || top == "default" || top == "?")) |
michael@0 | 575 | || (ttk == "while" && top == "do"); |
michael@0 | 576 | } |
michael@0 | 577 | |
michael@0 | 578 | /** |
michael@0 | 579 | * Returns true if the given token type should cause us to decrement the |
michael@0 | 580 | * indent level. |
michael@0 | 581 | */ |
michael@0 | 582 | function decrementsIndent(tokenType, stack) { |
michael@0 | 583 | return tokenType == "}" |
michael@0 | 584 | || (tokenType == "]" && stack[stack.length - 1] == "[\n") |
michael@0 | 585 | } |
michael@0 | 586 | |
michael@0 | 587 | /** |
michael@0 | 588 | * Returns true if the given token should cause us to increment the indent |
michael@0 | 589 | * level. |
michael@0 | 590 | */ |
michael@0 | 591 | function incrementsIndent(token) { |
michael@0 | 592 | return token.type.type == "{" || token.isArrayLiteral; |
michael@0 | 593 | } |
michael@0 | 594 | |
michael@0 | 595 | /** |
michael@0 | 596 | * Add a comment to the pretty printed code. |
michael@0 | 597 | * |
michael@0 | 598 | * @param Function write |
michael@0 | 599 | * The function to write pretty printed code to the result SourceNode. |
michael@0 | 600 | * @param Number indentLevel |
michael@0 | 601 | * The number of indents deep we are. |
michael@0 | 602 | * @param Object options |
michael@0 | 603 | * The options object. |
michael@0 | 604 | * @param Boolean block |
michael@0 | 605 | * True if the comment is a multiline block style comment. |
michael@0 | 606 | * @param String text |
michael@0 | 607 | * The text of the comment. |
michael@0 | 608 | * @param Number line |
michael@0 | 609 | * The line number to comment appeared on. |
michael@0 | 610 | * @param Number column |
michael@0 | 611 | * The column number the comment appeared on. |
michael@0 | 612 | */ |
michael@0 | 613 | function addComment(write, indentLevel, options, block, text, line, column) { |
michael@0 | 614 | var indentString = repeat(options.indent, indentLevel); |
michael@0 | 615 | |
michael@0 | 616 | write(indentString, line, column); |
michael@0 | 617 | if (block) { |
michael@0 | 618 | write("/*"); |
michael@0 | 619 | write(text |
michael@0 | 620 | .split(new RegExp("/\n" + indentString + "/", "g")) |
michael@0 | 621 | .join("\n" + indentString)); |
michael@0 | 622 | write("*/"); |
michael@0 | 623 | } else { |
michael@0 | 624 | write("//"); |
michael@0 | 625 | write(text); |
michael@0 | 626 | } |
michael@0 | 627 | write("\n"); |
michael@0 | 628 | } |
michael@0 | 629 | |
michael@0 | 630 | /** |
michael@0 | 631 | * The main function. |
michael@0 | 632 | * |
michael@0 | 633 | * @param String input |
michael@0 | 634 | * The ugly JS code we want to pretty print. |
michael@0 | 635 | * @param Object options |
michael@0 | 636 | * The options object. Provides configurability of the pretty |
michael@0 | 637 | * printing. Properties: |
michael@0 | 638 | * - url: The URL string of the ugly JS code. |
michael@0 | 639 | * - indent: The string to indent code by. |
michael@0 | 640 | * |
michael@0 | 641 | * @returns Object |
michael@0 | 642 | * An object with the following properties: |
michael@0 | 643 | * - code: The pretty printed code string. |
michael@0 | 644 | * - map: A SourceMapGenerator instance. |
michael@0 | 645 | */ |
michael@0 | 646 | return function prettyFast(input, options) { |
michael@0 | 647 | // The level of indents deep we are. |
michael@0 | 648 | var indentLevel = 0; |
michael@0 | 649 | |
michael@0 | 650 | // We will accumulate the pretty printed code in this SourceNode. |
michael@0 | 651 | var result = new SourceNode(); |
michael@0 | 652 | |
michael@0 | 653 | /** |
michael@0 | 654 | * Write a pretty printed string to the result SourceNode. |
michael@0 | 655 | * |
michael@0 | 656 | * We buffer our writes so that we only create one mapping for each line in |
michael@0 | 657 | * the source map. This enhances performance by avoiding extraneous mapping |
michael@0 | 658 | * serialization, and flattening the tree that |
michael@0 | 659 | * `SourceNode#toStringWithSourceMap` will have to recursively walk. When |
michael@0 | 660 | * timing how long it takes to pretty print jQuery, this optimization |
michael@0 | 661 | * brought the time down from ~390 ms to ~190ms! |
michael@0 | 662 | * |
michael@0 | 663 | * @param String str |
michael@0 | 664 | * The string to be added to the result. |
michael@0 | 665 | * @param Number line |
michael@0 | 666 | * The line number the string came from in the ugly source. |
michael@0 | 667 | * @param Number column |
michael@0 | 668 | * The column number the string came from in the ugly source. |
michael@0 | 669 | */ |
michael@0 | 670 | var write = (function () { |
michael@0 | 671 | var buffer = []; |
michael@0 | 672 | var bufferLine = -1; |
michael@0 | 673 | var bufferColumn = -1; |
michael@0 | 674 | return function write(str, line, column) { |
michael@0 | 675 | if (line != null && bufferLine === -1) { |
michael@0 | 676 | bufferLine = line; |
michael@0 | 677 | } |
michael@0 | 678 | if (column != null && bufferColumn === -1) { |
michael@0 | 679 | bufferColumn = column; |
michael@0 | 680 | } |
michael@0 | 681 | buffer.push(str); |
michael@0 | 682 | |
michael@0 | 683 | if (str == "\n") { |
michael@0 | 684 | var lineStr = ""; |
michael@0 | 685 | for (var i = 0, len = buffer.length; i < len; i++) { |
michael@0 | 686 | lineStr += buffer[i]; |
michael@0 | 687 | } |
michael@0 | 688 | result.add(new SourceNode(bufferLine, bufferColumn, options.url, lineStr)); |
michael@0 | 689 | buffer.splice(0, buffer.length); |
michael@0 | 690 | bufferLine = -1; |
michael@0 | 691 | bufferColumn = -1; |
michael@0 | 692 | } |
michael@0 | 693 | } |
michael@0 | 694 | }()); |
michael@0 | 695 | |
michael@0 | 696 | // Whether or not we added a newline on after we added the last token. |
michael@0 | 697 | var addedNewline = false; |
michael@0 | 698 | |
michael@0 | 699 | // The current token we will be adding to the pretty printed code. |
michael@0 | 700 | var token; |
michael@0 | 701 | |
michael@0 | 702 | // Shorthand for token.type.type, so we don't have to repeatedly access |
michael@0 | 703 | // properties. |
michael@0 | 704 | var ttt; |
michael@0 | 705 | |
michael@0 | 706 | // Shorthand for token.type.keyword, so we don't have to repeatedly access |
michael@0 | 707 | // properties. |
michael@0 | 708 | var ttk; |
michael@0 | 709 | |
michael@0 | 710 | // The last token we added to the pretty printed code. |
michael@0 | 711 | var lastToken; |
michael@0 | 712 | |
michael@0 | 713 | // Stack of token types/keywords that can affect whether we want to add a |
michael@0 | 714 | // newline or a space. We can make that decision based on what token type is |
michael@0 | 715 | // on the top of the stack. For example, a comma in a parameter list should |
michael@0 | 716 | // be followed by a space, while a comma in an object literal should be |
michael@0 | 717 | // followed by a newline. |
michael@0 | 718 | // |
michael@0 | 719 | // Strings that go on the stack: |
michael@0 | 720 | // |
michael@0 | 721 | // - "{" |
michael@0 | 722 | // - "(" |
michael@0 | 723 | // - "[" |
michael@0 | 724 | // - "[\n" |
michael@0 | 725 | // - "do" |
michael@0 | 726 | // - "?" |
michael@0 | 727 | // - "case" |
michael@0 | 728 | // - "default" |
michael@0 | 729 | // |
michael@0 | 730 | // The difference between "[" and "[\n" is that "[\n" is used when we are |
michael@0 | 731 | // treating "[" and "]" tokens as line delimiters and should increment and |
michael@0 | 732 | // decrement the indent level when we find them. |
michael@0 | 733 | var stack = []; |
michael@0 | 734 | |
michael@0 | 735 | // Acorn's tokenizer will always yield comments *before* the token they |
michael@0 | 736 | // follow (unless the very first thing in the source is a comment), so we |
michael@0 | 737 | // have to queue the comments in order to pretty print them in the correct |
michael@0 | 738 | // location. For example, the source file: |
michael@0 | 739 | // |
michael@0 | 740 | // foo |
michael@0 | 741 | // // a |
michael@0 | 742 | // // b |
michael@0 | 743 | // bar |
michael@0 | 744 | // |
michael@0 | 745 | // When tokenized by acorn, gives us the following token stream: |
michael@0 | 746 | // |
michael@0 | 747 | // [ '// a', '// b', foo, bar ] |
michael@0 | 748 | var commentQueue = []; |
michael@0 | 749 | |
michael@0 | 750 | var getToken = acorn.tokenize(input, { |
michael@0 | 751 | locations: true, |
michael@0 | 752 | sourceFile: options.url, |
michael@0 | 753 | onComment: function (block, text, start, end, startLoc, endLoc) { |
michael@0 | 754 | if (lastToken) { |
michael@0 | 755 | commentQueue.push({ |
michael@0 | 756 | block: block, |
michael@0 | 757 | text: text, |
michael@0 | 758 | line: startLoc.line, |
michael@0 | 759 | column: startLoc.column |
michael@0 | 760 | }); |
michael@0 | 761 | } else { |
michael@0 | 762 | addComment(write, indentLevel, options, block, text, startLoc.line, |
michael@0 | 763 | startLoc.column); |
michael@0 | 764 | addedNewline = true; |
michael@0 | 765 | } |
michael@0 | 766 | } |
michael@0 | 767 | }); |
michael@0 | 768 | |
michael@0 | 769 | while (true) { |
michael@0 | 770 | token = getToken(); |
michael@0 | 771 | |
michael@0 | 772 | ttk = token.type.keyword; |
michael@0 | 773 | ttt = token.type.type; |
michael@0 | 774 | |
michael@0 | 775 | if (ttt == "eof") { |
michael@0 | 776 | if (!addedNewline) { |
michael@0 | 777 | write("\n"); |
michael@0 | 778 | } |
michael@0 | 779 | break; |
michael@0 | 780 | } |
michael@0 | 781 | |
michael@0 | 782 | token.isArrayLiteral = isArrayLiteral(token, lastToken); |
michael@0 | 783 | |
michael@0 | 784 | if (belongsOnStack(token)) { |
michael@0 | 785 | if (token.isArrayLiteral) { |
michael@0 | 786 | stack.push("[\n"); |
michael@0 | 787 | } else { |
michael@0 | 788 | stack.push(ttt || ttk); |
michael@0 | 789 | } |
michael@0 | 790 | } |
michael@0 | 791 | |
michael@0 | 792 | if (decrementsIndent(ttt, stack)) { |
michael@0 | 793 | indentLevel--; |
michael@0 | 794 | } |
michael@0 | 795 | |
michael@0 | 796 | prependWhiteSpace(token, lastToken, addedNewline, write, options, |
michael@0 | 797 | indentLevel, stack); |
michael@0 | 798 | addToken(token, write, options); |
michael@0 | 799 | addedNewline = appendNewline(token, write, stack); |
michael@0 | 800 | |
michael@0 | 801 | if (shouldStackPop(token, stack)) { |
michael@0 | 802 | stack.pop(); |
michael@0 | 803 | } |
michael@0 | 804 | |
michael@0 | 805 | if (incrementsIndent(token)) { |
michael@0 | 806 | indentLevel++; |
michael@0 | 807 | } |
michael@0 | 808 | |
michael@0 | 809 | // Acorn's tokenizer re-uses tokens, so we have to copy the last token on |
michael@0 | 810 | // every iteration. We follow acorn's lead here, and reuse the lastToken |
michael@0 | 811 | // object the same way that acorn reuses the token object. This allows us |
michael@0 | 812 | // to avoid allocations and minimize GC pauses. |
michael@0 | 813 | if (!lastToken) { |
michael@0 | 814 | lastToken = { startLoc: {}, endLoc: {} }; |
michael@0 | 815 | } |
michael@0 | 816 | lastToken.start = token.start; |
michael@0 | 817 | lastToken.end = token.end; |
michael@0 | 818 | lastToken.startLoc.line = token.startLoc.line; |
michael@0 | 819 | lastToken.startLoc.column = token.startLoc.column; |
michael@0 | 820 | lastToken.endLoc.line = token.endLoc.line; |
michael@0 | 821 | lastToken.endLoc.column = token.endLoc.column; |
michael@0 | 822 | lastToken.type = token.type; |
michael@0 | 823 | lastToken.value = token.value; |
michael@0 | 824 | lastToken.isArrayLiteral = token.isArrayLiteral; |
michael@0 | 825 | |
michael@0 | 826 | // Apply all the comments that have been queued up. |
michael@0 | 827 | if (commentQueue.length) { |
michael@0 | 828 | if (!addedNewline) { |
michael@0 | 829 | write("\n"); |
michael@0 | 830 | } |
michael@0 | 831 | for (var i = 0, n = commentQueue.length; i < n; i++) { |
michael@0 | 832 | var comment = commentQueue[i]; |
michael@0 | 833 | addComment(write, indentLevel, options, comment.block, comment.text, |
michael@0 | 834 | comment.line, comment.column); |
michael@0 | 835 | } |
michael@0 | 836 | addedNewline = true; |
michael@0 | 837 | commentQueue.splice(0, commentQueue.length); |
michael@0 | 838 | } |
michael@0 | 839 | } |
michael@0 | 840 | |
michael@0 | 841 | return result.toStringWithSourceMap({ file: options.url }); |
michael@0 | 842 | }; |
michael@0 | 843 | |
michael@0 | 844 | }.bind(this))); |