toolkit/devtools/pretty-fast/pretty-fast.js

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

mercurial