├── .circleci └── config.yml ├── .eslintrc ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── __snapshots__ └── test.js.snap ├── bin └── pretty-fast ├── package-lock.json ├── package.json ├── pretty-fast.js └── test.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | node: circleci/node@1.1.6 4 | jobs: 5 | build-and-test: 6 | executor: 7 | name: node/default 8 | steps: 9 | - checkout 10 | - node/with-cache: 11 | steps: 12 | - run: npm install 13 | - run: npm test 14 | - run: npm run eslint 15 | workflows: 16 | build-and-test: 17 | jobs: 18 | - build-and-test -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | }, 5 | "env": { 6 | "browser": true, 7 | "node": true, 8 | "amd": true 9 | }, 10 | "globals": { 11 | }, 12 | "rules": { 13 | "camelcase": 1, 14 | "eqeqeq": 0, 15 | "quotes": [ 2, "double" ], 16 | "valid-jsdoc": 0, 17 | "no-use-before-define": 0, 18 | "no-underscore-dangle": 0, 19 | "no-octal-escape": 0, 20 | "no-unused-vars": 0, 21 | "no-comma-dangle": 0 22 | } 23 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | 10 | 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Nick Fitzgerald 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pretty Fast 2 | 3 | Pretty Fast is a source-map-generating JavaScript pretty printer, that is pretty 4 | fast. 5 | 6 | [![Build Status](https://travis-ci.org/mozilla/pretty-fast.png?branch=master)](https://travis-ci.org/mozilla/pretty-fast) 7 | 8 | ## Install 9 | 10 | npm install pretty-fast 11 | 12 | ## Usage 13 | 14 | var prettyFast = require("pretty-fast"); 15 | 16 | var uglyJS = "function ugly(){code()}"; 17 | 18 | var pretty = prettyFast(uglyJS, { 19 | url: "test.js", 20 | indent: " " 21 | }); 22 | 23 | console.log(pretty.code); 24 | // function ugly() { 25 | // code() 26 | // } 27 | 28 | console.log(pretty.map); 29 | // [object SourceMapGenerator] 30 | 31 | (See the [mozilla/source-map][0] library for information on SourceMapGenerator 32 | instances, and source maps in general.) 33 | 34 | [0]: https://github.com/mozilla/source-map 35 | 36 | ## Options 37 | 38 | * `url` - The URL of the JavaScript source being prettified. Used in the 39 | generated source map. If you are prettifying JS that isn't from a file or 40 | doesn't have a URL, you can use a dummy value, such as "(anonymous)". 41 | 42 | * `indent` - The string that you want your code indented by. Most people want 43 | one of `" "`, `" "`, or `"\t"`. 44 | 45 | * `ecmaVersion` - Indicates the ECMAScript version to parse. 46 | See acorn.parse documentation for more details. Defaults to `"latest"`. 47 | 48 | ## Issues 49 | 50 | [Please use Bugzilla](https://bugzilla.mozilla.org/enter_bug.cgi?product=Firefox&component=Developer%20Tools%3A%20Debugger) 51 | 52 | ## Goals 53 | 54 | * To be pretty fast, while still generating source maps. 55 | 56 | * To avoid fully parsing the source text; we should be able to get away with 57 | only a tokenizer and some heuristics. 58 | 59 | * Preserve comments. 60 | 61 | * Pretty Fast should be able to run inside Web Workers. 62 | 63 | ## Non-goals 64 | 65 | * Extreme configurability of types of indentation, where curly braces go, etc. 66 | 67 | * To be the very fastest pretty printer in the universe. This goal is 68 | unattainable given that generating source maps is a requirement. We just need 69 | to be Pretty Fast. 70 | 71 | * To perfectly pretty print *exactly* as you would have written the code. This 72 | falls out from both not wanting to support extreme configurability, and 73 | avoiding full on parsing. We just aim to pretty print Pretty Well. 74 | -------------------------------------------------------------------------------- /__snapshots__/test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ASI return 1`] = ` 4 | "function f() { 5 | return 6 | { 7 | } 8 | } 9 | " 10 | `; 11 | 12 | exports[`ASI return 2`] = ` 13 | Array [ 14 | "(1, 0) -> (1, 0)", 15 | "(2, 13) -> (2, 0)", 16 | "(3, 13) -> (3, 0)", 17 | "(3, 14) -> (4, 0)", 18 | "(4, 11) -> (5, 0)", 19 | ] 20 | `; 21 | 22 | exports[`Arrays 1`] = ` 23 | "var a = [ 24 | 1, 25 | 2, 26 | 3 27 | ]; 28 | " 29 | `; 30 | 31 | exports[`Arrays 2`] = ` 32 | Array [ 33 | "(1, 0) -> (1, 0)", 34 | "(1, 7) -> (2, 0)", 35 | "(1, 9) -> (3, 0)", 36 | "(1, 11) -> (4, 0)", 37 | "(1, 12) -> (5, 0)", 38 | ] 39 | `; 40 | 41 | exports[`Binary operators 1`] = ` 42 | "var a = 5 * 30; 43 | var b = 5 >> 3; 44 | " 45 | `; 46 | 47 | exports[`Binary operators 2`] = ` 48 | Array [ 49 | "(1, 0) -> (1, 0)", 50 | "(1, 11) -> (2, 0)", 51 | ] 52 | `; 53 | 54 | exports[`Bug 975477 don't move end of line comments to next line 1`] = ` 55 | "switch (request.action) { 56 | case 'show': //$NON-NLS-0$ 57 | if (localStorage.hideicon !== 'true') { //$NON-NLS-0$ 58 | chrome.pageAction.show(sender.tab.id); 59 | } 60 | break; 61 | case 'hide': /*Multiline 62 | Comment */ 63 | break; 64 | default: 65 | console.warn('unknown request'); //$NON-NLS-0$ 66 | // don't respond if you don't understand the message. 67 | return; 68 | } 69 | " 70 | `; 71 | 72 | exports[`Bug 975477 don't move end of line comments to next line 2`] = ` 73 | Array [ 74 | "(1, 0) -> (1, 0)", 75 | "(2, 13) -> (2, 0)", 76 | "(3, 15) -> (3, 0)", 77 | "(4, 17) -> (4, 0)", 78 | "(5, 15) -> (5, 0)", 79 | "(6, 15) -> (6, 0)", 80 | "(7, 13) -> (7, 0)", 81 | "(7, 13) -> (8, 0)", 82 | "(9, 15) -> (9, 0)", 83 | "(10, 13) -> (10, 0)", 84 | "(11, 15) -> (11, 0)", 85 | "(12, 15) -> (12, 0)", 86 | "(13, 15) -> (13, 0)", 87 | "(14, 11) -> (14, 0)", 88 | ] 89 | `; 90 | 91 | exports[`Bug 977082 - space between grouping operator and dot notation 1`] = ` 92 | "JSON.stringify(3).length; 93 | ([1, 94 | 2, 95 | 3]).length; 96 | (new Date()).toLocaleString(); 97 | " 98 | `; 99 | 100 | exports[`Bug 977082 - space between grouping operator and dot notation 2`] = ` 101 | Array [ 102 | "(1, 0) -> (1, 0)", 103 | "(2, 11) -> (2, 0)", 104 | "(2, 15) -> (3, 0)", 105 | "(2, 17) -> (4, 0)", 106 | "(3, 11) -> (5, 0)", 107 | ] 108 | `; 109 | 110 | exports[`Bug 1206633 - spaces in for of 1`] = ` 111 | "for (let tab of tabs) { 112 | } 113 | " 114 | `; 115 | 116 | exports[`Bug 1206633 - spaces in for of 2`] = ` 117 | Array [ 118 | "(1, 0) -> (1, 0)", 119 | "(1, 23) -> (2, 0)", 120 | ] 121 | `; 122 | 123 | exports[`Bug 1261971 - indentation after switch statement 1`] = ` 124 | "{ 125 | switch (x) { 126 | } 127 | if (y) { 128 | } 129 | done(); 130 | } 131 | " 132 | `; 133 | 134 | exports[`Bug 1261971 - indentation after switch statement 2`] = ` 135 | Array [ 136 | "(1, 0) -> (1, 0)", 137 | "(1, 1) -> (2, 0)", 138 | "(1, 11) -> (3, 0)", 139 | "(1, 12) -> (4, 0)", 140 | "(1, 18) -> (5, 0)", 141 | "(1, 19) -> (6, 0)", 142 | "(1, 26) -> (7, 0)", 143 | ] 144 | `; 145 | 146 | exports[`Bug pretty-sure-3 - escaping line and paragraph separators 1`] = ` 147 | "x = '\\\\u2029\\\\u2028'; 148 | " 149 | `; 150 | 151 | exports[`Bug pretty-sure-3 - escaping line and paragraph separators 2`] = ` 152 | Array [ 153 | "(1, 0) -> (1, 0)", 154 | ] 155 | `; 156 | 157 | exports[`Bug pretty-sure-4 - escaping null character before digit 1`] = ` 158 | "x = '\\\\x001'; 159 | " 160 | `; 161 | 162 | exports[`Bug pretty-sure-4 - escaping null character before digit 2`] = ` 163 | Array [ 164 | "(1, 0) -> (1, 0)", 165 | ] 166 | `; 167 | 168 | exports[`Bug pretty-sure-5 - empty multiline comment shouldn't throw exception 1`] = ` 169 | "{ 170 | /* 171 | */ 172 | return; 173 | } 174 | " 175 | `; 176 | 177 | exports[`Bug pretty-sure-5 - empty multiline comment shouldn't throw exception 2`] = ` 178 | Array [ 179 | "(1, 0) -> (1, 0)", 180 | "(2, 11) -> (2, 0)", 181 | "(2, 11) -> (3, 0)", 182 | "(4, 13) -> (4, 0)", 183 | "(5, 11) -> (5, 0)", 184 | ] 185 | `; 186 | 187 | exports[`Bug pretty-sure-6 - inline comment shouldn't move parenthesis to next line 1`] = ` 188 | "return /* inline comment */ (1 + 1); 189 | " 190 | `; 191 | 192 | exports[`Bug pretty-sure-6 - inline comment shouldn't move parenthesis to next line 2`] = ` 193 | Array [ 194 | "(1, 0) -> (1, 0)", 195 | ] 196 | `; 197 | 198 | exports[`Bug pretty-sure-7 - accessing a literal number property requires a space 1`] = ` 199 | "0 .toString() + x.toString(); 200 | " 201 | `; 202 | 203 | exports[`Bug pretty-sure-7 - accessing a literal number property requires a space 2`] = ` 204 | Array [ 205 | "(1, 0) -> (1, 0)", 206 | ] 207 | `; 208 | 209 | exports[`Bug pretty-sure-8 - return and yield only accept arguments when on the same line 1`] = ` 210 | "{ 211 | return 212 | (x) 213 | yield 214 | (x) 215 | yield 216 | * x 217 | } 218 | " 219 | `; 220 | 221 | exports[`Bug pretty-sure-8 - return and yield only accept arguments when on the same line 2`] = ` 222 | Array [ 223 | "(1, 0) -> (1, 0)", 224 | "(2, 13) -> (2, 0)", 225 | "(3, 13) -> (3, 0)", 226 | "(4, 13) -> (4, 0)", 227 | "(5, 13) -> (5, 0)", 228 | "(6, 13) -> (6, 0)", 229 | "(7, 13) -> (7, 0)", 230 | "(8, 11) -> (8, 0)", 231 | ] 232 | `; 233 | 234 | exports[`Bug pretty-sure-9 - accept unary operator at start of file 1`] = ` 235 | "+ 0 236 | " 237 | `; 238 | 239 | exports[`Bug pretty-sure-9 - accept unary operator at start of file 2`] = ` 240 | Array [ 241 | "(1, 0) -> (1, 0)", 242 | ] 243 | `; 244 | 245 | exports[`Class extension within a function 1`] = ` 246 | "(function () { 247 | class X extends Y { 248 | constructor() { 249 | } 250 | } 251 | }) () 252 | " 253 | `; 254 | 255 | exports[`Class extension within a function 2`] = ` 256 | Array [ 257 | "(1, 0) -> (1, 0)", 258 | "(1, 15) -> (2, 0)", 259 | "(1, 35) -> (3, 0)", 260 | "(1, 50) -> (4, 0)", 261 | "(1, 52) -> (5, 0)", 262 | "(1, 55) -> (6, 0)", 263 | ] 264 | `; 265 | 266 | exports[`Class handling 1`] = ` 267 | "class Class { 268 | constructor() { 269 | } 270 | } 271 | " 272 | `; 273 | 274 | exports[`Class handling 2`] = ` 275 | Array [ 276 | "(1, 0) -> (1, 0)", 277 | "(1, 13) -> (2, 0)", 278 | "(1, 27) -> (3, 0)", 279 | "(1, 28) -> (4, 0)", 280 | ] 281 | `; 282 | 283 | exports[`Code that relies on ASI 1`] = ` 284 | "var foo = 10 285 | var bar = 20 286 | function g() { 287 | a() 288 | b() 289 | } 290 | " 291 | `; 292 | 293 | exports[`Code that relies on ASI 2`] = ` 294 | Array [ 295 | "(2, 11) -> (1, 0)", 296 | "(3, 11) -> (2, 0)", 297 | "(4, 11) -> (3, 0)", 298 | "(5, 13) -> (4, 0)", 299 | "(6, 13) -> (5, 0)", 300 | "(7, 11) -> (6, 0)", 301 | ] 302 | `; 303 | 304 | exports[`Const handling 1`] = ` 305 | "const d = 'yes'; 306 | " 307 | `; 308 | 309 | exports[`Const handling 2`] = ` 310 | Array [ 311 | "(1, 0) -> (1, 0)", 312 | ] 313 | `; 314 | 315 | exports[`Continue/break statements 1`] = ` 316 | "while (1) { 317 | if (x) { 318 | continue 319 | } 320 | if (y) { 321 | break 322 | } 323 | if (z) { 324 | break foo 325 | } 326 | } 327 | " 328 | `; 329 | 330 | exports[`Continue/break statements 2`] = ` 331 | Array [ 332 | "(1, 0) -> (1, 0)", 333 | "(1, 9) -> (2, 0)", 334 | "(1, 15) -> (3, 0)", 335 | "(1, 23) -> (4, 0)", 336 | "(1, 24) -> (5, 0)", 337 | "(1, 30) -> (6, 0)", 338 | "(1, 35) -> (7, 0)", 339 | "(1, 36) -> (8, 0)", 340 | "(1, 42) -> (9, 0)", 341 | "(1, 51) -> (10, 0)", 342 | "(1, 52) -> (11, 0)", 343 | ] 344 | `; 345 | 346 | exports[`Delete 1`] = ` 347 | "delete obj.prop; 348 | " 349 | `; 350 | 351 | exports[`Delete 2`] = ` 352 | Array [ 353 | "(1, 0) -> (1, 0)", 354 | ] 355 | `; 356 | 357 | exports[`Do/while loop 1`] = ` 358 | "do { 359 | x 360 | } while (y) 361 | " 362 | `; 363 | 364 | exports[`Do/while loop 2`] = ` 365 | Array [ 366 | "(1, 0) -> (1, 0)", 367 | "(1, 3) -> (2, 0)", 368 | "(1, 4) -> (3, 0)", 369 | ] 370 | `; 371 | 372 | exports[`Dot handling with keywords which are identifier name 1`] = ` 373 | "y.await.break.const.delete.else.return.new.yield = 1.23; 374 | " 375 | `; 376 | 377 | exports[`Dot handling with keywords which are identifier name 2`] = ` 378 | Array [ 379 | "(1, 0) -> (1, 0)", 380 | ] 381 | `; 382 | 383 | exports[`Dot handling with let which is identifier name 1`] = ` 384 | "y.let.let = 1.23; 385 | " 386 | `; 387 | 388 | exports[`Dot handling with let which is identifier name 2`] = ` 389 | Array [ 390 | "(1, 0) -> (1, 0)", 391 | ] 392 | `; 393 | 394 | exports[`Escaping backslashes in strings 1`] = ` 395 | "'\\\\\\\\' 396 | " 397 | `; 398 | 399 | exports[`Escaping backslashes in strings 2`] = ` 400 | Array [ 401 | "(1, 0) -> (1, 0)", 402 | ] 403 | `; 404 | 405 | exports[`Escaping carriage return in strings 1`] = ` 406 | "'\\\\r' 407 | " 408 | `; 409 | 410 | exports[`Escaping carriage return in strings 2`] = ` 411 | Array [ 412 | "(1, 0) -> (1, 0)", 413 | ] 414 | `; 415 | 416 | exports[`Escaping form feed in strings 1`] = ` 417 | "'\\\\f' 418 | " 419 | `; 420 | 421 | exports[`Escaping form feed in strings 2`] = ` 422 | Array [ 423 | "(1, 0) -> (1, 0)", 424 | ] 425 | `; 426 | 427 | exports[`Escaping null character in strings 1`] = ` 428 | "'\\\\x00' 429 | " 430 | `; 431 | 432 | exports[`Escaping null character in strings 2`] = ` 433 | Array [ 434 | "(1, 0) -> (1, 0)", 435 | ] 436 | `; 437 | 438 | exports[`Escaping tab in strings 1`] = ` 439 | "'\\\\t' 440 | " 441 | `; 442 | 443 | exports[`Escaping tab in strings 2`] = ` 444 | Array [ 445 | "(1, 0) -> (1, 0)", 446 | ] 447 | `; 448 | 449 | exports[`Escaping vertical tab in strings 1`] = ` 450 | "'\\\\v' 451 | " 452 | `; 453 | 454 | exports[`Escaping vertical tab in strings 2`] = ` 455 | Array [ 456 | "(1, 0) -> (1, 0)", 457 | ] 458 | `; 459 | 460 | exports[`False assignment 1`] = ` 461 | "var foo = false; 462 | " 463 | `; 464 | 465 | exports[`False assignment 2`] = ` 466 | Array [ 467 | "(1, 0) -> (1, 0)", 468 | ] 469 | `; 470 | 471 | exports[`For loop 1`] = ` 472 | "for (var i = 0; i < n; i++) { 473 | console.log(i); 474 | } 475 | " 476 | `; 477 | 478 | exports[`For loop 2`] = ` 479 | Array [ 480 | "(1, 0) -> (1, 0)", 481 | "(1, 30) -> (2, 0)", 482 | "(1, 46) -> (3, 0)", 483 | ] 484 | `; 485 | 486 | exports[`Function calls 1`] = ` 487 | "var result = func(a, b, c, d); 488 | " 489 | `; 490 | 491 | exports[`Function calls 2`] = ` 492 | Array [ 493 | "(1, 0) -> (1, 0)", 494 | ] 495 | `; 496 | 497 | exports[`Getter and setter literals 1`] = ` 498 | "var obj = { 499 | get foo() { 500 | return this._foo 501 | }, 502 | set foo(v) { 503 | this._foo = v 504 | } 505 | } 506 | " 507 | `; 508 | 509 | exports[`Getter and setter literals 2`] = ` 510 | Array [ 511 | "(1, 0) -> (1, 0)", 512 | "(1, 9) -> (2, 0)", 513 | "(1, 19) -> (3, 0)", 514 | "(1, 35) -> (4, 0)", 515 | "(1, 37) -> (5, 0)", 516 | "(1, 48) -> (6, 0)", 517 | "(1, 59) -> (7, 0)", 518 | "(1, 60) -> (8, 0)", 519 | ] 520 | `; 521 | 522 | exports[`If/else statement 1`] = ` 523 | "if (c) { 524 | then() 525 | } else { 526 | other() 527 | } 528 | " 529 | `; 530 | 531 | exports[`If/else statement 2`] = ` 532 | Array [ 533 | "(1, 0) -> (1, 0)", 534 | "(1, 6) -> (2, 0)", 535 | "(1, 12) -> (3, 0)", 536 | "(1, 18) -> (4, 0)", 537 | "(1, 25) -> (5, 0)", 538 | ] 539 | `; 540 | 541 | exports[`If/else without curlies 1`] = ` 542 | "if (c) a else b 543 | " 544 | `; 545 | 546 | exports[`If/else without curlies 2`] = ` 547 | Array [ 548 | "(1, 0) -> (1, 0)", 549 | ] 550 | `; 551 | 552 | exports[`Immediately invoked function expression 1`] = ` 553 | "(function () { 554 | thingy() 555 | }()) 556 | " 557 | `; 558 | 559 | exports[`Immediately invoked function expression 2`] = ` 560 | Array [ 561 | "(1, 0) -> (1, 0)", 562 | "(1, 12) -> (2, 0)", 563 | "(1, 20) -> (3, 0)", 564 | ] 565 | `; 566 | 567 | exports[`In operator 1`] = ` 568 | "if (foo in bar) { 569 | doThing() 570 | } 571 | " 572 | `; 573 | 574 | exports[`In operator 2`] = ` 575 | Array [ 576 | "(1, 0) -> (1, 0)", 577 | "(1, 15) -> (2, 0)", 578 | "(1, 24) -> (3, 0)", 579 | ] 580 | `; 581 | 582 | exports[`Indented multiline comment 1`] = ` 583 | "function foo() { 584 | /** 585 | * java doc style comment 586 | * more comment 587 | */ 588 | bar(); 589 | } 590 | " 591 | `; 592 | 593 | exports[`Indented multiline comment 2`] = ` 594 | Array [ 595 | "(1, 0) -> (1, 0)", 596 | "(2, 13) -> (2, 0)", 597 | "(2, 13) -> (3, 0)", 598 | "(2, 13) -> (4, 0)", 599 | "(2, 13) -> (5, 0)", 600 | "(6, 13) -> (6, 0)", 601 | "(7, 11) -> (7, 0)", 602 | ] 603 | `; 604 | 605 | exports[`Instanceof 1`] = ` 606 | "var a = x instanceof y; 607 | " 608 | `; 609 | 610 | exports[`Instanceof 2`] = ` 611 | Array [ 612 | "(1, 0) -> (1, 0)", 613 | ] 614 | `; 615 | 616 | exports[`Let handling with value 1`] = ` 617 | "let d = 'yes'; 618 | " 619 | `; 620 | 621 | exports[`Let handling with value 2`] = ` 622 | Array [ 623 | "(1, 0) -> (1, 0)", 624 | ] 625 | `; 626 | 627 | exports[`Let handling without value 1`] = ` 628 | "let d; 629 | " 630 | `; 631 | 632 | exports[`Let handling without value 2`] = ` 633 | Array [ 634 | "(1, 0) -> (1, 0)", 635 | ] 636 | `; 637 | 638 | exports[`Multi line comment 1`] = ` 639 | "/* Comment 640 | more comment */ 641 | function foo() { 642 | bar(); 643 | } 644 | " 645 | `; 646 | 647 | exports[`Multi line comment 2`] = ` 648 | Array [ 649 | "(2, 4) -> (1, 0)", 650 | "(2, 4) -> (2, 0)", 651 | "(4, 4) -> (3, 0)", 652 | "(4, 21) -> (4, 0)", 653 | "(4, 28) -> (5, 0)", 654 | ] 655 | `; 656 | 657 | exports[`Multiple single line comments 1`] = ` 658 | "function f() { 659 | // a 660 | // b 661 | // c 662 | } 663 | " 664 | `; 665 | 666 | exports[`Multiple single line comments 2`] = ` 667 | Array [ 668 | "(1, 0) -> (1, 0)", 669 | "(2, 13) -> (2, 0)", 670 | "(3, 13) -> (3, 0)", 671 | "(4, 13) -> (4, 0)", 672 | "(5, 11) -> (5, 0)", 673 | ] 674 | `; 675 | 676 | exports[`Named class handling 1`] = ` 677 | "let unnamed = class Class { 678 | constructor() { 679 | } 680 | } 681 | " 682 | `; 683 | 684 | exports[`Named class handling 2`] = ` 685 | Array [ 686 | "(1, 0) -> (1, 0)", 687 | "(1, 24) -> (2, 0)", 688 | "(1, 38) -> (3, 0)", 689 | "(1, 39) -> (4, 0)", 690 | ] 691 | `; 692 | 693 | exports[`Nested function 1`] = ` 694 | "function foo() { 695 | function bar() { 696 | debugger; 697 | } 698 | bar(); 699 | } 700 | " 701 | `; 702 | 703 | exports[`Nested function 2`] = ` 704 | Array [ 705 | "(1, 0) -> (1, 0)", 706 | "(1, 17) -> (2, 0)", 707 | "(1, 34) -> (3, 0)", 708 | "(1, 44) -> (4, 0)", 709 | "(1, 46) -> (5, 0)", 710 | "(1, 53) -> (6, 0)", 711 | ] 712 | `; 713 | 714 | exports[`New expression 1`] = ` 715 | "var foo = new Foo(); 716 | " 717 | `; 718 | 719 | exports[`New expression 2`] = ` 720 | Array [ 721 | "(1, 0) -> (1, 0)", 722 | ] 723 | `; 724 | 725 | exports[`Non-ASI function call 1`] = ` 726 | "f() 727 | " 728 | `; 729 | 730 | exports[`Non-ASI function call 2`] = ` 731 | Array [ 732 | "(1, 0) -> (1, 0)", 733 | ] 734 | `; 735 | 736 | exports[`Non-ASI in 1`] = ` 737 | "'x' in foo 738 | " 739 | `; 740 | 741 | exports[`Non-ASI in 2`] = ` 742 | Array [ 743 | "(1, 0) -> (1, 0)", 744 | ] 745 | `; 746 | 747 | exports[`Non-ASI new 1`] = ` 748 | "new F() 749 | " 750 | `; 751 | 752 | exports[`Non-ASI new 2`] = ` 753 | Array [ 754 | "(1, 0) -> (1, 0)", 755 | ] 756 | `; 757 | 758 | exports[`Non-ASI property access 1`] = ` 759 | "[ 760 | 1, 761 | 2, 762 | 3 763 | ] 764 | [0] 765 | " 766 | `; 767 | 768 | exports[`Non-ASI property access 2`] = ` 769 | Array [ 770 | "(1, 0) -> (1, 0)", 771 | "(1, 1) -> (2, 0)", 772 | "(1, 3) -> (3, 0)", 773 | "(1, 5) -> (4, 0)", 774 | "(1, 6) -> (5, 0)", 775 | "(2, 11) -> (6, 0)", 776 | ] 777 | `; 778 | 779 | exports[`Null assignment 1`] = ` 780 | "var i = null; 781 | " 782 | `; 783 | 784 | exports[`Null assignment 2`] = ` 785 | Array [ 786 | "(1, 0) -> (1, 0)", 787 | ] 788 | `; 789 | 790 | exports[`Objects 1`] = ` 791 | "var o = { 792 | a: 1, 793 | b: 2 794 | }; 795 | " 796 | `; 797 | 798 | exports[`Objects 2`] = ` 799 | Array [ 800 | "(2, 6) -> (1, 0)", 801 | "(2, 13) -> (2, 0)", 802 | "(3, 6) -> (3, 0)", 803 | "(3, 9) -> (4, 0)", 804 | ] 805 | `; 806 | 807 | exports[`Optional chaining parsing support 1`] = ` 808 | "x?.y?.z?.['a']?.check(); 809 | " 810 | `; 811 | 812 | exports[`Optional chaining parsing support 2`] = ` 813 | Array [ 814 | "(1, 0) -> (1, 0)", 815 | ] 816 | `; 817 | 818 | exports[`Private fields parsing support 1`] = ` 819 | "class MyClass { 820 | constructor(a) { 821 | this.#a = a; 822 | this.#b = Math.random(); 823 | this.ab = this.#getAB(); 824 | } 825 | #a 826 | #b = 'default value' 827 | static #someStaticPrivate 828 | #getA() { 829 | return this.#a; 830 | } 831 | #getAB() { 832 | return this.#getA() + this.#b 833 | } 834 | } 835 | " 836 | `; 837 | 838 | exports[`Private fields parsing support 2`] = ` 839 | Array [ 840 | "(2, 6) -> (1, 0)", 841 | "(3, 8) -> (2, 0)", 842 | "(4, 10) -> (3, 0)", 843 | "(4, 22) -> (4, 0)", 844 | "(4, 46) -> (5, 0)", 845 | "(5, 8) -> (6, 0)", 846 | "(6, 8) -> (7, 0)", 847 | "(7, 8) -> (8, 0)", 848 | "(8, 8) -> (9, 0)", 849 | "(9, 8) -> (10, 0)", 850 | "(10, 10) -> (11, 0)", 851 | "(11, 8) -> (12, 0)", 852 | "(12, 8) -> (13, 0)", 853 | "(13, 10) -> (14, 0)", 854 | "(15, 8) -> (15, 0)", 855 | "(16, 6) -> (16, 0)", 856 | ] 857 | `; 858 | 859 | exports[`Regexp 1`] = ` 860 | "var r = /foobar/g; 861 | " 862 | `; 863 | 864 | exports[`Regexp 2`] = ` 865 | Array [ 866 | "(1, 0) -> (1, 0)", 867 | ] 868 | `; 869 | 870 | exports[`Simple function 1`] = ` 871 | "function foo() { 872 | bar(); 873 | } 874 | " 875 | `; 876 | 877 | exports[`Simple function 2`] = ` 878 | Array [ 879 | "(1, 0) -> (1, 0)", 880 | "(1, 17) -> (2, 0)", 881 | "(1, 24) -> (3, 0)", 882 | ] 883 | `; 884 | 885 | exports[`Single line comment 1`] = ` 886 | "// Comment 887 | function foo() { 888 | bar(); 889 | } 890 | " 891 | `; 892 | 893 | exports[`Single line comment 2`] = ` 894 | Array [ 895 | "(2, 4) -> (1, 0)", 896 | "(3, 4) -> (2, 0)", 897 | "(3, 21) -> (3, 0)", 898 | "(3, 28) -> (4, 0)", 899 | ] 900 | `; 901 | 902 | exports[`Stack-keyword property access 1`] = ` 903 | "foo.a = 1.1; 904 | foo.do.switch.case.default = 2.2; 905 | foo.b = 3.3; 906 | " 907 | `; 908 | 909 | exports[`Stack-keyword property access 2`] = ` 910 | Array [ 911 | "(1, 0) -> (1, 0)", 912 | "(1, 10) -> (2, 0)", 913 | "(1, 41) -> (3, 0)", 914 | ] 915 | `; 916 | 917 | exports[`String with quote 1`] = ` 918 | "var foo = '\\\\''; 919 | " 920 | `; 921 | 922 | exports[`String with quote 2`] = ` 923 | Array [ 924 | "(1, 0) -> (1, 0)", 925 | ] 926 | `; 927 | 928 | exports[`String with semicolon 1`] = ` 929 | "var foo = ';'; 930 | " 931 | `; 932 | 933 | exports[`String with semicolon 2`] = ` 934 | Array [ 935 | "(1, 0) -> (1, 0)", 936 | ] 937 | `; 938 | 939 | exports[`Subclass handling 1`] = ` 940 | "class Class extends Base { 941 | constructor() { 942 | } 943 | } 944 | " 945 | `; 946 | 947 | exports[`Subclass handling 2`] = ` 948 | Array [ 949 | "(1, 0) -> (1, 0)", 950 | "(1, 28) -> (2, 0)", 951 | "(1, 42) -> (3, 0)", 952 | "(1, 43) -> (4, 0)", 953 | ] 954 | `; 955 | 956 | exports[`Switch statements 1`] = ` 957 | "switch (x) { 958 | case a: 959 | foo(); 960 | break; 961 | default: 962 | bar() 963 | } 964 | " 965 | `; 966 | 967 | exports[`Switch statements 2`] = ` 968 | Array [ 969 | "(1, 0) -> (1, 0)", 970 | "(1, 10) -> (2, 0)", 971 | "(1, 17) -> (3, 0)", 972 | "(1, 23) -> (4, 0)", 973 | "(1, 29) -> (5, 0)", 974 | "(1, 37) -> (6, 0)", 975 | "(1, 42) -> (7, 0)", 976 | ] 977 | `; 978 | 979 | exports[`Template literals 1`] = ` 980 | "\`abc\${ JSON.stringify({ 981 | clas: 'testing' 982 | }) }def\`; 983 | { 984 | a(); 985 | } 986 | " 987 | `; 988 | 989 | exports[`Template literals 2`] = ` 990 | Array [ 991 | "(1, 0) -> (1, 0)", 992 | "(1, 22) -> (2, 0)", 993 | "(1, 37) -> (3, 0)", 994 | "(1, 45) -> (4, 0)", 995 | "(1, 46) -> (5, 0)", 996 | "(1, 50) -> (6, 0)", 997 | ] 998 | `; 999 | 1000 | exports[`Ternary operator 1`] = ` 1001 | "bar ? baz : bang; 1002 | " 1003 | `; 1004 | 1005 | exports[`Ternary operator 2`] = ` 1006 | Array [ 1007 | "(1, 0) -> (1, 0)", 1008 | ] 1009 | `; 1010 | 1011 | exports[`This property access 1`] = ` 1012 | "var foo = this.foo; 1013 | " 1014 | `; 1015 | 1016 | exports[`This property access 2`] = ` 1017 | Array [ 1018 | "(1, 0) -> (1, 0)", 1019 | ] 1020 | `; 1021 | 1022 | exports[`True assignment 1`] = ` 1023 | "var foo = true; 1024 | " 1025 | `; 1026 | 1027 | exports[`True assignment 2`] = ` 1028 | Array [ 1029 | "(1, 0) -> (1, 0)", 1030 | ] 1031 | `; 1032 | 1033 | exports[`Try/catch/finally statement 1`] = ` 1034 | "try { 1035 | dangerous() 1036 | } catch (e) { 1037 | handle(e) 1038 | } finally { 1039 | cleanup() 1040 | } 1041 | " 1042 | `; 1043 | 1044 | exports[`Try/catch/finally statement 2`] = ` 1045 | Array [ 1046 | "(1, 0) -> (1, 0)", 1047 | "(1, 4) -> (2, 0)", 1048 | "(1, 15) -> (3, 0)", 1049 | "(1, 25) -> (4, 0)", 1050 | "(1, 34) -> (5, 0)", 1051 | "(1, 43) -> (6, 0)", 1052 | "(1, 52) -> (7, 0)", 1053 | ] 1054 | `; 1055 | 1056 | exports[`Undefined assignment 1`] = ` 1057 | "var i = undefined; 1058 | " 1059 | `; 1060 | 1061 | exports[`Undefined assignment 2`] = ` 1062 | Array [ 1063 | "(1, 0) -> (1, 0)", 1064 | ] 1065 | `; 1066 | 1067 | exports[`Unnamed class handling 1`] = ` 1068 | "let unnamed = class { 1069 | constructor() { 1070 | } 1071 | } 1072 | " 1073 | `; 1074 | 1075 | exports[`Unnamed class handling 2`] = ` 1076 | Array [ 1077 | "(1, 0) -> (1, 0)", 1078 | "(1, 18) -> (2, 0)", 1079 | "(1, 32) -> (3, 0)", 1080 | "(1, 33) -> (4, 0)", 1081 | ] 1082 | `; 1083 | 1084 | exports[`Void 0 assignment 1`] = ` 1085 | "var i = void 0; 1086 | " 1087 | `; 1088 | 1089 | exports[`Void 0 assignment 2`] = ` 1090 | Array [ 1091 | "(1, 0) -> (1, 0)", 1092 | ] 1093 | `; 1094 | 1095 | exports[`With statement 1`] = ` 1096 | "with (obj) { 1097 | crock() 1098 | } 1099 | " 1100 | `; 1101 | 1102 | exports[`With statement 2`] = ` 1103 | Array [ 1104 | "(1, 0) -> (1, 0)", 1105 | "(1, 10) -> (2, 0)", 1106 | "(1, 17) -> (3, 0)", 1107 | ] 1108 | `; 1109 | -------------------------------------------------------------------------------- /bin/pretty-fast: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* -*- Mode: javascript; -*- */ 3 | /* 4 | * Copyright 2013 Mozilla Foundation and contributors 5 | * Licensed under the New BSD license. See LICENSE.md or: 6 | * http://opensource.org/licenses/BSD-2-Clause 7 | */ 8 | 9 | var fs = require("fs"); 10 | var path = require("path"); 11 | var prettyFast = require("pretty-fast"); 12 | 13 | var argv = require('optimist') 14 | .usage("Pretty print a JavaScript file and generate a source map.\n\nUsage:\n\n\t$0 [options] file") 15 | .options("o", { 16 | alias: "out", 17 | describe: "Output file. Defaults to stdout.", 18 | default: "-", 19 | }) 20 | .options("m", { 21 | alias: "map", 22 | describe: "Source map output file.", 23 | default: null, 24 | }) 25 | .options("i", { 26 | alias: "indent", 27 | describe: "The indentation string.", 28 | default: " " 29 | }) 30 | .options("t", { 31 | alias: "time", 32 | describe: "Just time how long it takes to pretty print the input file.", 33 | default: false 34 | }) 35 | .check(function (argv) { 36 | return argv._.length === 1; 37 | }) 38 | .argv; 39 | 40 | function main() { 41 | var inputFile = argv._[0]; 42 | fs.readFile(inputFile, function (err, data) { 43 | if (err) { 44 | throw err; 45 | } 46 | 47 | var start = Date.now(); 48 | var result = prettyFast(data, { 49 | url: inputFile, 50 | indent: argv.indent 51 | }); 52 | 53 | if (argv.time) { 54 | console.log(Date.now() - start); 55 | } else { 56 | writeOut(argv.out, argv.map, result.code); 57 | writeMap(argv.map, result.map); 58 | } 59 | }); 60 | } 61 | 62 | function writeOut(file, mapFile, code) { 63 | if (mapFile) { 64 | code += "\n//# sourceMappingURL=" + path.relative(path.dirname(file), 65 | mapFile); 66 | } 67 | 68 | if (file == "-") { 69 | console.log(code); 70 | } else { 71 | fs.writeFile(file, code, ifErrorThrow); 72 | } 73 | } 74 | 75 | function writeMap(file, map) { 76 | if (file) { 77 | fs.writeFile(file, map.toString(), ifErrorThrow); 78 | } 79 | } 80 | 81 | function ifErrorThrow(err) { 82 | if (err) { 83 | throw err; 84 | } 85 | } 86 | 87 | main(); 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pretty-fast", 3 | "version": "0.2.7", 4 | "description": "A fast JavaScript pretty printer.", 5 | "author": "Nick Fitzgerald ", 6 | "homepage": "https://github.com/mozilla/pretty-fast", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/mozilla/pretty-fast.git" 10 | }, 11 | "license": "BSD", 12 | "main": "pretty-fast.js", 13 | "scripts": { 14 | "eslint": "eslint -f compact *.js", 15 | "test": "jest" 16 | }, 17 | "bin": { 18 | "pretty-fast": "./bin/pretty-fast" 19 | }, 20 | "devDependencies": { 21 | "chalk": "^3.0.0", 22 | "eslint": "6.6.0", 23 | "jest": "^25.1.0" 24 | }, 25 | "dependencies": { 26 | "source-map": "^0.5.7", 27 | "acorn": "~8.2.4", 28 | "optimist": "~0.6.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pretty-fast.js: -------------------------------------------------------------------------------- 1 | /* -*- indent-tabs-mode: nil; js-indent-level: 2; fill-column: 80 -*- */ 2 | /* 3 | * Copyright 2013 Mozilla Foundation and contributors 4 | * Licensed under the New BSD license. See LICENSE.md or: 5 | * http://opensource.org/licenses/BSD-2-Clause 6 | */ 7 | (function (root, factory) { 8 | "use strict"; 9 | 10 | if (typeof define === "function" && define.amd) { 11 | define(factory); 12 | } else if (typeof exports === "object") { 13 | module.exports = factory(); 14 | } else { 15 | root.prettyFast = factory(); 16 | } 17 | }(this, function () { 18 | "use strict"; 19 | 20 | var acorn = this.acorn || require("acorn"); 21 | var sourceMap = this.sourceMap || require("source-map"); 22 | var SourceNode = sourceMap.SourceNode; 23 | 24 | // If any of these tokens are seen before a "[" token, we know that "[" token 25 | // is the start of an array literal, rather than a property access. 26 | // 27 | // The only exception is "}", which would need to be disambiguated by 28 | // parsing. The majority of the time, an open bracket following a closing 29 | // curly is going to be an array literal, so we brush the complication under 30 | // the rug, and handle the ambiguity by always assuming that it will be an 31 | // array literal. 32 | var PRE_ARRAY_LITERAL_TOKENS = { 33 | "typeof": true, 34 | "void": true, 35 | "delete": true, 36 | "case": true, 37 | "do": true, 38 | "=": true, 39 | "in": true, 40 | "{": true, 41 | "*": true, 42 | "/": true, 43 | "%": true, 44 | "else": true, 45 | ";": true, 46 | "++": true, 47 | "--": true, 48 | "+": true, 49 | "-": true, 50 | "~": true, 51 | "!": true, 52 | ":": true, 53 | "?": true, 54 | ">>": true, 55 | ">>>": true, 56 | "<<": true, 57 | "||": true, 58 | "&&": true, 59 | "<": true, 60 | ">": true, 61 | "<=": true, 62 | ">=": true, 63 | "instanceof": true, 64 | "&": true, 65 | "^": true, 66 | "|": true, 67 | "==": true, 68 | "!=": true, 69 | "===": true, 70 | "!==": true, 71 | ",": true, 72 | 73 | "}": true 74 | }; 75 | 76 | /** 77 | * Determines if we think that the given token starts an array literal. 78 | * 79 | * @param Object token 80 | * The token we want to determine if it is an array literal. 81 | * @param Object lastToken 82 | * The last token we added to the pretty printed results. 83 | * 84 | * @returns Boolean 85 | * True if we believe it is an array literal, false otherwise. 86 | */ 87 | function isArrayLiteral(token, lastToken) { 88 | if (token.type.label != "[") { 89 | return false; 90 | } 91 | if (!lastToken) { 92 | return true; 93 | } 94 | if (lastToken.type.isAssign) { 95 | return true; 96 | } 97 | return !!PRE_ARRAY_LITERAL_TOKENS[ 98 | lastToken.type.keyword || lastToken.type.label 99 | ]; 100 | } 101 | 102 | // If any of these tokens are followed by a token on a new line, we know that 103 | // ASI cannot happen. 104 | var PREVENT_ASI_AFTER_TOKENS = { 105 | // Binary operators 106 | "*": true, 107 | "/": true, 108 | "%": true, 109 | "+": true, 110 | "-": true, 111 | "<<": true, 112 | ">>": true, 113 | ">>>": true, 114 | "<": true, 115 | ">": true, 116 | "<=": true, 117 | ">=": true, 118 | "instanceof": true, 119 | "in": true, 120 | "==": true, 121 | "!=": true, 122 | "===": true, 123 | "!==": true, 124 | "&": true, 125 | "^": true, 126 | "|": true, 127 | "&&": true, 128 | "||": true, 129 | ",": true, 130 | ".": true, 131 | "=": true, 132 | "*=": true, 133 | "/=": true, 134 | "%=": true, 135 | "+=": true, 136 | "-=": true, 137 | "<<=": true, 138 | ">>=": true, 139 | ">>>=": true, 140 | "&=": true, 141 | "^=": true, 142 | "|=": true, 143 | // Unary operators 144 | "delete": true, 145 | "void": true, 146 | "typeof": true, 147 | "~": true, 148 | "!": true, 149 | "new": true, 150 | // Function calls and grouped expressions 151 | "(": true 152 | }; 153 | 154 | // If any of these tokens are on a line after the token before it, we know 155 | // that ASI cannot happen. 156 | var PREVENT_ASI_BEFORE_TOKENS = { 157 | // Binary operators 158 | "*": true, 159 | "/": true, 160 | "%": true, 161 | "<<": true, 162 | ">>": true, 163 | ">>>": true, 164 | "<": true, 165 | ">": true, 166 | "<=": true, 167 | ">=": true, 168 | "instanceof": true, 169 | "in": true, 170 | "==": true, 171 | "!=": true, 172 | "===": true, 173 | "!==": true, 174 | "&": true, 175 | "^": true, 176 | "|": true, 177 | "&&": true, 178 | "||": true, 179 | ",": true, 180 | ".": true, 181 | "=": true, 182 | "*=": true, 183 | "/=": true, 184 | "%=": true, 185 | "+=": true, 186 | "-=": true, 187 | "<<=": true, 188 | ">>=": true, 189 | ">>>=": true, 190 | "&=": true, 191 | "^=": true, 192 | "|=": true, 193 | // Function calls 194 | "(": true 195 | }; 196 | 197 | /** 198 | * Determine if a token can look like an identifier. More precisely, 199 | * this determines if the token may end or start with a character from 200 | * [A-Za-z0-9_]. 201 | * 202 | * @param Object token 203 | * The token we are looking at. 204 | * 205 | * @returns Boolean 206 | * True if identifier-like. 207 | */ 208 | function isIdentifierLike(token) { 209 | var ttl = token.type.label; 210 | return ttl == "name" || ttl == "num" || ttl == "privateId" || !!token.type.keyword; 211 | } 212 | 213 | /** 214 | * Determines if Automatic Semicolon Insertion (ASI) occurs between these 215 | * tokens. 216 | * 217 | * @param Object token 218 | * The current token. 219 | * @param Object lastToken 220 | * The last token we added to the pretty printed results. 221 | * 222 | * @returns Boolean 223 | * True if we believe ASI occurs. 224 | */ 225 | function isASI(token, lastToken) { 226 | if (!lastToken) { 227 | return false; 228 | } 229 | if (token.loc.start.line === lastToken.loc.start.line) { 230 | return false; 231 | } 232 | if (lastToken.type.keyword == "return" 233 | || lastToken.type.keyword == "yield" 234 | || (lastToken.type.label == "name" && lastToken.value == "yield")) { 235 | return true; 236 | } 237 | if (PREVENT_ASI_AFTER_TOKENS[ 238 | lastToken.type.label || lastToken.type.keyword 239 | ]) { 240 | return false; 241 | } 242 | if (PREVENT_ASI_BEFORE_TOKENS[token.type.label || token.type.keyword]) { 243 | return false; 244 | } 245 | return true; 246 | } 247 | 248 | /** 249 | * Determine if we should add a newline after the given token. 250 | * 251 | * @param Object token 252 | * The token we are looking at. 253 | * @param Array stack 254 | * The stack of open parens/curlies/brackets/etc. 255 | * 256 | * @returns Boolean 257 | * True if we should add a newline. 258 | */ 259 | function isLineDelimiter(token, stack) { 260 | if (token.isArrayLiteral) { 261 | return true; 262 | } 263 | var ttl = token.type.label; 264 | var top = stack[stack.length - 1]; 265 | return ttl == ";" && top != "(" 266 | || ttl == "{" 267 | || ttl == "," && top != "(" 268 | || ttl == ":" && (top == "case" || top == "default"); 269 | } 270 | 271 | /** 272 | * Append the necessary whitespace to the result after we have added the given 273 | * token. 274 | * 275 | * @param Object token 276 | * The token that was just added to the result. 277 | * @param Function write 278 | * The function to write to the pretty printed results. 279 | * @param Array stack 280 | * The stack of open parens/curlies/brackets/etc. 281 | * 282 | * @returns Boolean 283 | * Returns true if we added a newline to result, false in all other 284 | * cases. 285 | */ 286 | function appendNewline(token, write, stack) { 287 | if (isLineDelimiter(token, stack)) { 288 | write("\n", token.loc.start.line, token.loc.start.column); 289 | return true; 290 | } 291 | return false; 292 | } 293 | 294 | /** 295 | * Determines if we need to add a space between the last token we added and 296 | * the token we are about to add. 297 | * 298 | * @param Object token 299 | * The token we are about to add to the pretty printed code. 300 | * @param Object lastToken 301 | * The last token added to the pretty printed code. 302 | */ 303 | function needsSpaceAfter(token, lastToken) { 304 | if (lastToken) { 305 | if (lastToken.type.isLoop) { 306 | return true; 307 | } 308 | if (lastToken.type.isAssign) { 309 | return true; 310 | } 311 | if (lastToken.type.binop != null) { 312 | return true; 313 | } 314 | 315 | var ltt = lastToken.type.label; 316 | if (ltt == "?") { 317 | return true; 318 | } 319 | if (ltt == ":") { 320 | return true; 321 | } 322 | if (ltt == ",") { 323 | return true; 324 | } 325 | if (ltt == ";") { 326 | return true; 327 | } 328 | if (ltt == "${") { 329 | return true; 330 | } 331 | if (ltt == "num" && token.type.label == ".") { 332 | return true; 333 | } 334 | 335 | var ltk = lastToken.type.keyword; 336 | var ttl = token.type.label; 337 | if (ltk != null && ttl != ".") { 338 | if (ltk == "break" || ltk == "continue" || ltk == "return") { 339 | return token.type.label != ";"; 340 | } 341 | if (ltk != "debugger" 342 | && ltk != "null" 343 | && ltk != "true" 344 | && ltk != "false" 345 | && ltk != "this" 346 | && ltk != "default") { 347 | return true; 348 | } 349 | } 350 | 351 | if (ltt == ")" && (token.type.label != ")" 352 | && token.type.label != "]" 353 | && token.type.label != ";" 354 | && token.type.label != "," 355 | && token.type.label != ".")) { 356 | return true; 357 | } 358 | 359 | if (isIdentifierLike(token) && isIdentifierLike(lastToken)) { 360 | // We must emit a space to avoid merging the tokens. 361 | return true; 362 | } 363 | 364 | if (token.type.label == "{" && lastToken.type.label == "name") { 365 | return true; 366 | } 367 | } 368 | 369 | if (token.type.isAssign) { 370 | return true; 371 | } 372 | if (token.type.binop != null && lastToken) { 373 | return true; 374 | } 375 | if (token.type.label == "?") { 376 | return true; 377 | } 378 | 379 | return false; 380 | } 381 | 382 | /** 383 | * Add the required whitespace before this token, whether that is a single 384 | * space, newline, and/or the indent on fresh lines. 385 | * 386 | * @param Object token 387 | * The token we are about to add to the pretty printed code. 388 | * @param Object lastToken 389 | * The last token we added to the pretty printed code. 390 | * @param Boolean addedNewline 391 | * Whether we added a newline after adding the last token to the pretty 392 | * printed code. 393 | * @param Boolean addedSpace 394 | * Whether we added a space after adding the last token to the pretty 395 | * printed code. This only happens if an inline comment was printed 396 | * since the last token. 397 | * @param Function write 398 | * The function to write pretty printed code to the result SourceNode. 399 | * @param Object options 400 | * The options object. 401 | * @param Number indentLevel 402 | * The number of indents deep we are. 403 | * @param Array stack 404 | * The stack of open curlies, brackets, etc. 405 | */ 406 | function prependWhiteSpace(token, lastToken, addedNewline, addedSpace, write, options, 407 | indentLevel, stack) { 408 | var ttk = token.type.keyword; 409 | var ttl = token.type.label; 410 | var newlineAdded = addedNewline; 411 | var spaceAdded = addedSpace; 412 | var ltt = lastToken ? lastToken.type.label : null; 413 | 414 | // Handle whitespace and newlines after "}" here instead of in 415 | // `isLineDelimiter` because it is only a line delimiter some of the 416 | // time. For example, we don't want to put "else if" on a new line after 417 | // the first if's block. 418 | if (lastToken && ltt == "}") { 419 | if (ttk == "while" && stack[stack.length - 1] == "do") { 420 | write(" ", 421 | lastToken.loc.start.line, 422 | lastToken.loc.start.column); 423 | spaceAdded = true; 424 | } else if (ttk == "else" || 425 | ttk == "catch" || 426 | ttk == "finally") { 427 | write(" ", 428 | lastToken.loc.start.line, 429 | lastToken.loc.start.column); 430 | spaceAdded = true; 431 | } else if (ttl != "(" && 432 | ttl != ";" && 433 | ttl != "," && 434 | ttl != ")" && 435 | ttl != "." && 436 | ttl != "template" && 437 | ttl != "`") { 438 | write("\n", 439 | lastToken.loc.start.line, 440 | lastToken.loc.start.column); 441 | newlineAdded = true; 442 | } 443 | } 444 | 445 | if ((ttl == ":" && stack[stack.length - 1] == "?") || (ttl == "}" && stack[stack.length - 1] == "${")) { 446 | write(" ", 447 | lastToken.loc.start.line, 448 | lastToken.loc.start.column); 449 | spaceAdded = true; 450 | } 451 | 452 | if (lastToken && ltt != "}" && ltt != "." && ttk == "else") { 453 | write(" ", 454 | lastToken.loc.start.line, 455 | lastToken.loc.start.column); 456 | spaceAdded = true; 457 | } 458 | 459 | function ensureNewline() { 460 | if (!newlineAdded) { 461 | write("\n", 462 | lastToken.loc.start.line, 463 | lastToken.loc.start.column); 464 | newlineAdded = true; 465 | } 466 | } 467 | 468 | if (isASI(token, lastToken)) { 469 | ensureNewline(); 470 | } 471 | 472 | if (decrementsIndent(ttl, stack)) { 473 | ensureNewline(); 474 | } 475 | 476 | if (newlineAdded) { 477 | if (ttk == "case" || ttk == "default") { 478 | write(repeat(options.indent, indentLevel - 1), 479 | token.loc.start.line, 480 | token.loc.start.column); 481 | } else { 482 | write(repeat(options.indent, indentLevel), 483 | token.loc.start.line, 484 | token.loc.start.column); 485 | } 486 | } else if (!spaceAdded && needsSpaceAfter(token, lastToken)) { 487 | write(" ", 488 | lastToken.loc.start.line, 489 | lastToken.loc.start.column); 490 | spaceAdded = true; 491 | } 492 | } 493 | 494 | /** 495 | * Repeat the `str` string `n` times. 496 | * 497 | * @param String str 498 | * The string to be repeated. 499 | * @param Number n 500 | * The number of times to repeat the string. 501 | * 502 | * @returns String 503 | * The repeated string. 504 | */ 505 | function repeat(str, n) { 506 | var result = ""; 507 | while (n > 0) { 508 | if (n & 1) { 509 | result += str; 510 | } 511 | n >>= 1; 512 | str += str; 513 | } 514 | return result; 515 | } 516 | 517 | /** 518 | * Make sure that we output the escaped character combination inside string 519 | * literals instead of various problematic characters. 520 | */ 521 | var sanitize = (function () { 522 | var escapeCharacters = { 523 | // Backslash 524 | "\\": "\\\\", 525 | // Newlines 526 | "\n": "\\n", 527 | // Carriage return 528 | "\r": "\\r", 529 | // Tab 530 | "\t": "\\t", 531 | // Vertical tab 532 | "\v": "\\v", 533 | // Form feed 534 | "\f": "\\f", 535 | // Null character 536 | "\0": "\\x00", 537 | // Line separator 538 | "\u2028": "\\u2028", 539 | // Paragraph separator 540 | "\u2029": "\\u2029", 541 | // Single quotes 542 | "'": "\\'" 543 | }; 544 | 545 | var regExpString = "(" 546 | + Object.keys(escapeCharacters) 547 | .map(function (c) { return escapeCharacters[c]; }) 548 | .join("|") 549 | + ")"; 550 | var escapeCharactersRegExp = new RegExp(regExpString, "g"); 551 | 552 | return function (str) { 553 | return str.replace(escapeCharactersRegExp, function (_, c) { 554 | return escapeCharacters[c]; 555 | }); 556 | }; 557 | }()); 558 | /** 559 | * Add the given token to the pretty printed results. 560 | * 561 | * @param Object token 562 | * The token to add. 563 | * @param Function write 564 | * The function to write pretty printed code to the result SourceNode. 565 | */ 566 | function addToken(token, write) { 567 | if (token.type.label == "string") { 568 | write("'" + sanitize(token.value) + "'", 569 | token.loc.start.line, 570 | token.loc.start.column); 571 | } else if (token.type.label == "regexp") { 572 | write(String(token.value.value), 573 | token.loc.start.line, 574 | token.loc.start.column); 575 | } else { 576 | let value; 577 | if (token.value != null) { 578 | value = token.value; 579 | if (token.type.label === "privateId") { 580 | value = `#${value}`; 581 | } 582 | } else { 583 | value = token.type.label; 584 | } 585 | write(String(value), 586 | token.loc.start.line, 587 | token.loc.start.column); 588 | } 589 | } 590 | 591 | /** 592 | * Returns true if the given token type belongs on the stack. 593 | */ 594 | function belongsOnStack(token) { 595 | var ttl = token.type.label; 596 | var ttk = token.type.keyword; 597 | return ttl == "{" 598 | || ttl == "(" 599 | || ttl == "[" 600 | || ttl == "?" 601 | || ttl == "${" 602 | || ttk == "do" 603 | || ttk == "switch" 604 | || ttk == "case" 605 | || ttk == "default"; 606 | } 607 | 608 | /** 609 | * Returns true if the given token should cause us to pop the stack. 610 | */ 611 | function shouldStackPop(token, stack) { 612 | var ttl = token.type.label; 613 | var ttk = token.type.keyword; 614 | var top = stack[stack.length - 1]; 615 | return ttl == "]" 616 | || ttl == ")" 617 | || ttl == "}" 618 | || (ttl == ":" && (top == "case" || top == "default" || top == "?")) 619 | || (ttk == "while" && top == "do"); 620 | } 621 | 622 | /** 623 | * Returns true if the given token type should cause us to decrement the 624 | * indent level. 625 | */ 626 | function decrementsIndent(tokenType, stack) { 627 | return (tokenType == "}" && stack[stack.length - 1] != "${") 628 | || (tokenType == "]" && stack[stack.length - 1] == "[\n"); 629 | } 630 | 631 | /** 632 | * Returns true if the given token should cause us to increment the indent 633 | * level. 634 | */ 635 | function incrementsIndent(token) { 636 | return token.type.label == "{" 637 | || token.isArrayLiteral 638 | || token.type.keyword == "switch"; 639 | } 640 | 641 | /** 642 | * Add a comment to the pretty printed code. 643 | * 644 | * @param Function write 645 | * The function to write pretty printed code to the result SourceNode. 646 | * @param Number indentLevel 647 | * The number of indents deep we are. 648 | * @param Object options 649 | * The options object. 650 | * @param Boolean block 651 | * True if the comment is a multiline block style comment. 652 | * @param String text 653 | * The text of the comment. 654 | * @param Number line 655 | * The line number to comment appeared on. 656 | * @param Number column 657 | * The column number the comment appeared on. 658 | * @param Object nextToken 659 | * The next token if any. 660 | * 661 | * @returns Boolean newlineAdded 662 | * True if a newline was added. 663 | */ 664 | function addComment(write, indentLevel, options, block, text, line, column, nextToken) { 665 | var indentString = repeat(options.indent, indentLevel); 666 | var needNewline = true; 667 | 668 | write(indentString, line, column); 669 | if (block) { 670 | write("/*"); 671 | // We must pass ignoreNewline in case the comment happens to be "\n". 672 | write(text 673 | .split(new RegExp("/\n" + indentString + "/", "g")) 674 | .join("\n" + indentString), 675 | null, null, true); 676 | write("*/"); 677 | needNewline = !(nextToken && nextToken.loc.start.line == line); 678 | } else { 679 | write("//"); 680 | write(text); 681 | } 682 | if (needNewline) { 683 | write("\n"); 684 | } else { 685 | write(" "); 686 | } 687 | return needNewline; 688 | } 689 | 690 | /** 691 | * The main function. 692 | * 693 | * @param String input 694 | * The ugly JS code we want to pretty print. 695 | * @param Object options 696 | * The options object. Provides configurability of the pretty 697 | * printing. Properties: 698 | * - url: The URL string of the ugly JS code. 699 | * - indent: The string to indent code by. 700 | * 701 | * @returns Object 702 | * An object with the following properties: 703 | * - code: The pretty printed code string. 704 | * - map: A SourceMapGenerator instance. 705 | */ 706 | return function prettyFast(input, options) { 707 | // The level of indents deep we are. 708 | var indentLevel = 0; 709 | 710 | // We will accumulate the pretty printed code in this SourceNode. 711 | var result = new SourceNode(); 712 | 713 | /** 714 | * Write a pretty printed string to the result SourceNode. 715 | * 716 | * We buffer our writes so that we only create one mapping for each line in 717 | * the source map. This enhances performance by avoiding extraneous mapping 718 | * serialization, and flattening the tree that 719 | * `SourceNode#toStringWithSourceMap` will have to recursively walk. When 720 | * timing how long it takes to pretty print jQuery, this optimization 721 | * brought the time down from ~390 ms to ~190ms! 722 | * 723 | * @param String str 724 | * The string to be added to the result. 725 | * @param Number line 726 | * The line number the string came from in the ugly source. 727 | * @param Number column 728 | * The column number the string came from in the ugly source. 729 | * @param Boolean ignoreNewline 730 | * If true, a single "\n" won't result in an additional mapping. 731 | */ 732 | var write = (function () { 733 | var buffer = []; 734 | var bufferLine = -1; 735 | var bufferColumn = -1; 736 | return function write(str, line, column, ignoreNewline) { 737 | if (line != null && bufferLine === -1) { 738 | bufferLine = line; 739 | } 740 | if (column != null && bufferColumn === -1) { 741 | bufferColumn = column; 742 | } 743 | buffer.push(str); 744 | 745 | if (str == "\n" && !ignoreNewline) { 746 | var lineStr = ""; 747 | for (var i = 0, len = buffer.length; i < len; i++) { 748 | lineStr += buffer[i]; 749 | } 750 | result.add(new SourceNode(bufferLine, bufferColumn, options.url, 751 | lineStr)); 752 | buffer.splice(0, buffer.length); 753 | bufferLine = -1; 754 | bufferColumn = -1; 755 | } 756 | }; 757 | }()); 758 | 759 | // Whether or not we added a newline on after we added the last token. 760 | var addedNewline = false; 761 | 762 | // Whether or not we added a space after we added the last token. 763 | var addedSpace = false; 764 | 765 | // The current token we will be adding to the pretty printed code. 766 | var token; 767 | 768 | // Shorthand for token.type.label, so we don't have to repeatedly access 769 | // properties. 770 | var ttl; 771 | 772 | // Shorthand for token.type.keyword, so we don't have to repeatedly access 773 | // properties. 774 | var ttk; 775 | 776 | // The last token we added to the pretty printed code. 777 | var lastToken; 778 | 779 | // Stack of token types/keywords that can affect whether we want to add a 780 | // newline or a space. We can make that decision based on what token type is 781 | // on the top of the stack. For example, a comma in a parameter list should 782 | // be followed by a space, while a comma in an object literal should be 783 | // followed by a newline. 784 | // 785 | // Strings that go on the stack: 786 | // 787 | // - "{" 788 | // - "(" 789 | // - "[" 790 | // - "[\n" 791 | // - "do" 792 | // - "?" 793 | // - "switch" 794 | // - "case" 795 | // - "default" 796 | // 797 | // The difference between "[" and "[\n" is that "[\n" is used when we are 798 | // treating "[" and "]" tokens as line delimiters and should increment and 799 | // decrement the indent level when we find them. 800 | var stack = []; 801 | 802 | // Pass through acorn's tokenizer and append tokens and comments into a 803 | // single queue to process. For example, the source file: 804 | // 805 | // foo 806 | // // a 807 | // // b 808 | // bar 809 | // 810 | // After this process, tokenQueue has the following token stream: 811 | // 812 | // [ foo, '// a', '// b', bar] 813 | var tokenQueue = []; 814 | 815 | var tokens = acorn.tokenizer(input, { 816 | locations: true, 817 | sourceFile: options.url, 818 | ecmaVersion: options.ecmaVersion || "latest", 819 | onComment: function (block, text, start, end, startLoc, endLoc) { 820 | tokenQueue.push({ 821 | type: {}, 822 | comment: true, 823 | block: block, 824 | text: text, 825 | loc: { start: startLoc, end: endLoc } 826 | }); 827 | } 828 | }); 829 | 830 | for (;;) { 831 | token = tokens.getToken(); 832 | tokenQueue.push(token); 833 | if (token.type.label == "eof") { 834 | break; 835 | } 836 | } 837 | 838 | for (var i = 0; i < tokenQueue.length; i++) { 839 | token = tokenQueue[i]; 840 | var nextToken = tokenQueue[i + 1]; 841 | 842 | if (token.comment) { 843 | var commentIndentLevel = indentLevel; 844 | if (lastToken && (lastToken.loc.end.line == token.loc.start.line)) { 845 | commentIndentLevel = 0; 846 | write(" "); 847 | } 848 | addedNewline = addComment(write, commentIndentLevel, options, 849 | token.block, token.text, 850 | token.loc.start.line, token.loc.start.column, 851 | nextToken); 852 | addedSpace = !addedNewline; 853 | continue; 854 | } 855 | 856 | ttk = token.type.keyword; 857 | 858 | if (ttk && lastToken && lastToken.type.label == ".") { 859 | token.type = acorn.tokTypes.name; 860 | } 861 | 862 | ttl = token.type.label; 863 | 864 | if (ttl == "eof") { 865 | if (!addedNewline) { 866 | write("\n"); 867 | } 868 | break; 869 | } 870 | 871 | token.isArrayLiteral = isArrayLiteral(token, lastToken); 872 | 873 | if (belongsOnStack(token)) { 874 | if (token.isArrayLiteral) { 875 | stack.push("[\n"); 876 | } else { 877 | stack.push(ttl || ttk); 878 | } 879 | } 880 | 881 | if (decrementsIndent(ttl, stack)) { 882 | indentLevel--; 883 | if (ttl == "}" 884 | && stack.length > 1 885 | && stack[stack.length - 2] == "switch") { 886 | indentLevel--; 887 | } 888 | } 889 | 890 | prependWhiteSpace(token, lastToken, addedNewline, addedSpace, write, options, 891 | indentLevel, stack); 892 | addToken(token, write); 893 | addedSpace = false; 894 | 895 | // If the next token is going to be a comment starting on the same line, 896 | // then no need to add one here 897 | if (!nextToken || !nextToken.comment || token.loc.end.line != nextToken.loc.start.line) { 898 | addedNewline = appendNewline(token, write, stack); 899 | } 900 | 901 | if (shouldStackPop(token, stack)) { 902 | stack.pop(); 903 | if (ttl == "}" && stack.length 904 | && stack[stack.length - 1] == "switch") { 905 | stack.pop(); 906 | } 907 | } 908 | 909 | if (incrementsIndent(token)) { 910 | indentLevel++; 911 | } 912 | 913 | // Acorn's tokenizer re-uses tokens, so we have to copy the last token on 914 | // every iteration. We follow acorn's lead here, and reuse the lastToken 915 | // object the same way that acorn reuses the token object. This allows us 916 | // to avoid allocations and minimize GC pauses. 917 | if (!lastToken) { 918 | lastToken = { loc: { start: {}, end: {} } }; 919 | } 920 | lastToken.start = token.start; 921 | lastToken.end = token.end; 922 | lastToken.loc.start.line = token.loc.start.line; 923 | lastToken.loc.start.column = token.loc.start.column; 924 | lastToken.loc.end.line = token.loc.end.line; 925 | lastToken.loc.end.column = token.loc.end.column; 926 | lastToken.type = token.type; 927 | lastToken.value = token.value; 928 | lastToken.isArrayLiteral = token.isArrayLiteral; 929 | } 930 | 931 | return result.toStringWithSourceMap({ file: options.url }); 932 | }; 933 | 934 | }.bind(this))); 935 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* 4 | * Copyright 2013 Mozilla Foundation and contributors 5 | * Licensed under the New BSD license. See LICENSE.md or: 6 | * http://opensource.org/licenses/BSD-2-Clause 7 | */ 8 | const prettyFast = require("./pretty-fast"); 9 | const chalk = require("chalk") 10 | 11 | const cases = [ 12 | { 13 | name: "Simple function", 14 | input: "function foo() { bar(); }" 15 | }, 16 | { 17 | name: "Nested function", 18 | input: "function foo() { function bar() { debugger; } bar(); }" 19 | }, 20 | { 21 | name: "Immediately invoked function expression", 22 | input: "(function(){thingy()}())", 23 | }, 24 | { 25 | name: "Single line comment", 26 | input: ` 27 | // Comment 28 | function foo() { bar(); }` 29 | }, 30 | { 31 | name: "Multi line comment", 32 | input: ` 33 | /* Comment 34 | more comment */ 35 | function foo() { bar(); } 36 | ` 37 | }, 38 | {name: "Null assignment", input: "var i=null;"}, 39 | {name: "Undefined assignment", input: "var i=undefined;"}, 40 | {name: "Void 0 assignment", input: "var i=void 0;",}, 41 | { 42 | name: "This property access", 43 | input: "var foo=this.foo;\n", 44 | }, 45 | 46 | { 47 | name: "True assignment", 48 | input: "var foo=true;\n", 49 | }, 50 | 51 | { 52 | name: "False assignment", 53 | input: "var foo=false;\n", 54 | }, 55 | 56 | { 57 | name: "For loop", 58 | input: "for (var i = 0; i < n; i++) { console.log(i); }", 59 | }, 60 | 61 | { 62 | name: "String with semicolon", 63 | input: "var foo = ';';\n", 64 | }, 65 | 66 | { 67 | name: "String with quote", 68 | input: "var foo = \"'\";\n", 69 | }, 70 | 71 | { 72 | name: "Function calls", 73 | input: "var result=func(a,b,c,d);", 74 | }, 75 | 76 | { 77 | name: "Regexp", 78 | input: "var r=/foobar/g;", 79 | }, 80 | 81 | { 82 | name: "In operator", 83 | input: "if(foo in bar){doThing()}", 84 | output: "if (foo in bar) {\n" + 85 | " doThing()\n" + 86 | "}\n" 87 | }, 88 | { 89 | name: "With statement", 90 | input: "with(obj){crock()}", 91 | }, 92 | { 93 | name: "New expression", 94 | input: "var foo=new Foo();", 95 | }, 96 | { 97 | name: "Continue/break statements", 98 | input: "while(1){if(x){continue}if(y){break}if(z){break foo}}", 99 | }, 100 | { 101 | name: "Instanceof", 102 | input: "var a=x instanceof y;", 103 | }, 104 | { 105 | name: "Binary operators", 106 | input: "var a=5*30;var b=5>>3;", 107 | }, 108 | { 109 | name: "Delete", 110 | input: "delete obj.prop;", 111 | }, 112 | 113 | { 114 | name: "Try/catch/finally statement", 115 | input: "try{dangerous()}catch(e){handle(e)}finally{cleanup()}", 116 | }, 117 | { 118 | name: "If/else statement", 119 | input: "if(c){then()}else{other()}", 120 | }, 121 | { 122 | name: "If/else without curlies", 123 | input: "if(c) a else b", 124 | }, 125 | { 126 | name: "Objects", 127 | input: ` 128 | var o={a:1, 129 | b:2};` 130 | }, 131 | { 132 | name: "Do/while loop", 133 | input: "do{x}while(y)", 134 | }, 135 | { 136 | name: "Arrays", 137 | input: "var a=[1,2,3];", 138 | }, 139 | { 140 | name: "Code that relies on ASI", 141 | input: ` 142 | var foo = 10 143 | var bar = 20 144 | function g() { 145 | a() 146 | b() 147 | }` 148 | }, 149 | { 150 | name: "Ternary operator", 151 | input: "bar?baz:bang;", 152 | }, 153 | { 154 | name: "Switch statements", 155 | input: "switch(x){case a:foo();break;default:bar()}", 156 | }, 157 | 158 | { 159 | name: "Multiple single line comments", 160 | input: `function f() { 161 | // a 162 | // b 163 | // c 164 | }` 165 | }, 166 | { 167 | name: "Indented multiline comment", 168 | input: `function foo() { 169 | /** 170 | * java doc style comment 171 | * more comment 172 | */ 173 | bar(); 174 | }` 175 | }, 176 | { 177 | name: "ASI return", 178 | input: `function f() { 179 | return 180 | {} 181 | }` 182 | }, 183 | { 184 | name: "Non-ASI property access", 185 | input: `[1,2,3] 186 | [0]`, 187 | }, 188 | { 189 | name: "Non-ASI in", 190 | input: `'x' 191 | in foo` 192 | }, 193 | 194 | { 195 | name: "Non-ASI function call", 196 | input: `f 197 | ()`, 198 | }, 199 | { 200 | name: "Non-ASI new", 201 | input: `new 202 | F()` 203 | }, 204 | { 205 | name: "Getter and setter literals", 206 | input: "var obj={get foo(){return this._foo},set foo(v){this._foo=v}}", 207 | }, 208 | { 209 | name: "Escaping backslashes in strings", 210 | input: "'\\\\'\n", 211 | }, 212 | { 213 | name: "Escaping carriage return in strings", 214 | input: "'\\r'\n", 215 | }, 216 | { 217 | name: "Escaping tab in strings", 218 | input: "'\\t'\n", 219 | }, 220 | { 221 | name: "Escaping vertical tab in strings", 222 | input: "'\\v'\n", 223 | }, 224 | { 225 | name: "Escaping form feed in strings", 226 | input: "'\\f'\n", 227 | }, 228 | { 229 | name: "Escaping null character in strings", 230 | input: "'\\0'\n", 231 | }, 232 | { 233 | name: "Bug 977082 - space between grouping operator and dot notation", 234 | input: `JSON.stringify(3).length; 235 | ([1,2,3]).length; 236 | (new Date()).toLocaleString();`, 237 | }, 238 | { 239 | name: "Bug 975477 don't move end of line comments to next line", 240 | input: `switch (request.action) { 241 | case 'show': //$NON-NLS-0$ 242 | if (localStorage.hideicon !== 'true') { //$NON-NLS-0$ 243 | chrome.pageAction.show(sender.tab.id); 244 | } 245 | break; 246 | case 'hide': /*Multiline 247 | Comment */ 248 | break; 249 | default: 250 | console.warn('unknown request'); //$NON-NLS-0$ 251 | // don't respond if you don't understand the message. 252 | return; 253 | }` , 254 | }, 255 | { 256 | name: "Const handling", 257 | input: "const d = 'yes';\n", 258 | }, 259 | { 260 | name: "Let handling without value", 261 | input: "let d;\n", 262 | }, 263 | { 264 | name: "Let handling with value", 265 | input: "let d = 'yes';\n", 266 | }, 267 | { 268 | name: "Template literals", 269 | // issue in acorn 270 | input: "\`abc${JSON.stringify({clas: 'testing'})}def\`;{a();}", 271 | }, 272 | { 273 | name: "Class handling", 274 | input: "class Class{constructor(){}}", 275 | }, 276 | { 277 | name: "Subclass handling", 278 | input: "class Class extends Base{constructor(){}}", 279 | }, 280 | { 281 | name: "Unnamed class handling", 282 | input: "let unnamed=class{constructor(){}}", 283 | }, 284 | { 285 | name: "Named class handling", 286 | input: "let unnamed=class Class{constructor(){}}", 287 | }, 288 | { 289 | name: "Class extension within a function", 290 | input: "(function() { class X extends Y { constructor() {} } })()", 291 | }, 292 | { 293 | name: "Bug 1261971 - indentation after switch statement", 294 | input: "{switch(x){}if(y){}done();}", 295 | }, 296 | { 297 | name: "Bug 1206633 - spaces in for of", 298 | input: "for (let tab of tabs) {}", 299 | }, 300 | { 301 | name: "Bug pretty-sure-3 - escaping line and paragraph separators", 302 | input: "x = '\\u2029\\u2028';", 303 | }, 304 | { 305 | name: "Bug pretty-sure-4 - escaping null character before digit", 306 | input: "x = '\\u00001';", 307 | }, 308 | { 309 | name: "Bug pretty-sure-5 - empty multiline comment shouldn't throw exception", 310 | input: `{ 311 | /* 312 | */ 313 | return; 314 | }`, 315 | }, 316 | { 317 | name: "Bug pretty-sure-6 - inline comment shouldn't move parenthesis to next line", 318 | input: `return /* inline comment */ ( 319 | 1+1);`, 320 | }, 321 | { 322 | name: "Bug pretty-sure-7 - accessing a literal number property requires a space", 323 | input: "0..toString()+x.toString();", 324 | }, 325 | { 326 | name: "Bug pretty-sure-8 - return and yield only accept arguments when on the same line", 327 | input: `{ 328 | return 329 | (x) 330 | yield 331 | (x) 332 | yield 333 | *x 334 | }`, 335 | }, 336 | { 337 | name: "Bug pretty-sure-9 - accept unary operator at start of file", 338 | input: "+ 0", 339 | }, 340 | { 341 | name: "Stack-keyword property access", 342 | input: "foo.a=1.1;foo.do.switch.case.default=2.2;foo.b=3.3;\n", 343 | }, 344 | { 345 | name: "Dot handling with let which is identifier name", 346 | input: "y.let.let = 1.23;\n", 347 | }, 348 | { 349 | name: "Dot handling with keywords which are identifier name", 350 | input: "y.await.break.const.delete.else.return.new.yield = 1.23;\n", 351 | }, 352 | { 353 | name: "Optional chaining parsing support", 354 | input: "x?.y?.z?.['a']?.check();\n", 355 | }, 356 | { 357 | name: "Private fields parsing support", 358 | input: ` 359 | class MyClass { 360 | constructor(a) { 361 | this.#a = a;this.#b = Math.random();this.ab = this.#getAB(); 362 | } 363 | #a 364 | #b = "default value" 365 | static #someStaticPrivate 366 | #getA() { 367 | return this.#a; 368 | } 369 | #getAB() { 370 | return this.#getA()+this. 371 | #b 372 | } 373 | } 374 | `, 375 | } 376 | ]; 377 | 378 | const sourceMap = this.sourceMap || require("source-map"); 379 | const includesOnly = cases.find(({only}) => only); 380 | 381 | let output = `` 382 | 383 | for (const {name, input, only, skip} of cases) { 384 | if (includesOnly && !only || skip) { 385 | continue; 386 | } 387 | test(name, () => { 388 | const actual = prettyFast(input, { 389 | indent: " ", 390 | url: "test.js" 391 | }); 392 | 393 | expect(actual.code).toMatchSnapshot(); 394 | 395 | output += `${chalk.bold(name)}\n${chalk.yellow(input.trim())}\n${chalk.blue(actual.code)}\n` 396 | const smc = new sourceMap.SourceMapConsumer(actual.map.toJSON()); 397 | 398 | const mappings = [] 399 | smc.eachMapping(({ generatedColumn, generatedLine, originalColumn, originalLine }) => { 400 | mappings.push(`(${originalLine}, ${originalColumn}) -> (${generatedLine}, ${generatedColumn})`) 401 | }); 402 | expect(mappings).toMatchSnapshot(); 403 | }) 404 | } 405 | 406 | afterAll(() => { 407 | // Enable to view the test run output 408 | if (false) { 409 | console.log(output) 410 | } 411 | }) 412 | --------------------------------------------------------------------------------