function OutputStream(options) { options = defaults(options, { indent_start : 0, indent_level : 4, quote_keys : false, space_colon : false, ascii_only : false, inline_script : false, width : 80, beautify : true, scope_style : "negate" }); var indentation = 0; var current_col = 0; var current_line = 0; var current_pos = 0; var OUTPUT = ""; function to_ascii(str) { return str.replace(/[\u0080-\uffff]/g, function(ch) { var code = ch.charCodeAt(0).toString(16); while (code.length < 4) code = "0" + code; return "\\u" + code; }); }; function make_string(str) { var dq = 0, sq = 0; str = str.replace(/[\\\b\f\n\r\t\x22\x27\u2028\u2029\0]/g, function(s){ switch (s) { case "\\": return "\\\\"; case "\b": return "\\b"; case "\f": return "\\f"; case "\n": return "\\n"; case "\r": return "\\r"; case "\u2028": return "\\u2028"; case "\u2029": return "\\u2029"; case '"': ++dq; return '"'; case "'": ++sq; return "'"; case "\0": return "\\0"; } return s; }); if (options.ascii_only) str = to_ascii(str); if (dq > sq) return "'" + str.replace(/\x27/g, "\\'") + "'"; else return '"' + str.replace(/\x22/g, '\\"') + '"'; }; function encode_string(str) { var ret = make_string(str); if (options.inline_script) ret = ret.replace(/<\x2fscript([>\/\t\n\f\r ])/gi, "<\\/script$1"); return ret; }; function make_name(name) { name = name.toString(); if (options.ascii_only) name = to_ascii(name); return name; }; function make_indent(back) { return repeat_string(" ", options.indent_start + indentation - back * options.indent_level); }; /* -----[ beautification/minification ]----- */ var might_need_space = false; var might_need_semicolon = false; var last = null; function last_char() { return last.charAt(last.length - 1); }; function print(str) { str = String(str); var ch = str.charAt(0); if (might_need_semicolon) { if (";}".indexOf(ch) < 0 && !/[;]$/.test(last)) { OUTPUT += ";"; current_col++; current_pos++; if (!options.beautify) might_need_space = false; } might_need_semicolon = false; } if (might_need_space) { if ((is_identifier_char(last_char()) && (is_identifier_char(ch) || ch == "\\")) || (/[\+\-]$/.test(last) && /^[\+\-]/.test(str))) { OUTPUT += " "; current_col++; current_pos++; } might_need_space = false; } var a = str.split(/\r?\n/), n = a.length; current_line += n; if (n == 1) { current_col = a[n - 1].length; } else { current_col += a[n - 1].length; } current_pos += str.length; last = str; OUTPUT += str; }; var space = options.beautify ? function() { print(" "); } : function() { might_need_space = true; }; var indent = options.beautify ? function(half) { if (options.beautify) { print(make_indent(half ? 0.5 : 0)); } } : noop; var with_indent = options.beautify ? function(col, cont) { if (col === true) col = next_indent(); var save_indentation = indentation; indentation = col; var ret = cont(); indentation = save_indentation; return ret; } : function(col, cont) { return cont() }; var newline = options.beautify ? function() { print("\n"); } : noop; var semicolon = options.beautify ? function() { print(";"); } : function() { might_need_semicolon = true; }; function next_indent() { return indentation + options.indent_level; }; function with_block(cont) { var ret; print("{"); newline(); with_indent(next_indent(), function(){ ret = cont(); }); indent(); print("}"); return ret; }; function with_parens(cont) { print("("); //XXX: still nice to have that for argument lists //var ret = with_indent(current_col, cont); var ret = cont(); print(")"); return ret; }; function with_square(cont) { print("["); //var ret = with_indent(current_col, cont); var ret = cont(); print("]"); return ret; }; function comma() { print(","); space(); }; function colon() { print(":"); space(); }; var stack = []; return { get : function() { return OUTPUT }, indent : indent, newline : newline, print : print, space : space, comma : comma, colon : colon, last : function() { return last }, semicolon : semicolon, print_name : function(name) { print(make_name(name)) }, print_string : function(str) { print(encode_string(str)) }, with_indent : with_indent, with_block : with_block, with_parens : with_parens, with_square : with_square, options : function(opt) { return options[opt] }, line : function() { return current_line }, col : function() { return current_col }, pos : function() { return current_pos }, push_node : function(node) { stack.push(node) }, pop_node : function() { return stack.pop() }, stack : function() { return stack }, parent : function(n) { return stack[stack.length - 2 - (n || 0)]; } }; }; /* -----[ code generators ]----- */ (function(){ /* -----[ utils ]----- */ function DEFPRINT(nodetype, generator) { nodetype.DEFMETHOD("print", function(stream){ var self = this; stream.push_node(self); //stream.print("«" + self.TYPE + ":" + self.start.line + ":" + self.start.col + "»"); if (self.needs_parens(stream)) { stream.with_parens(function(){ generator(self, stream); }); } else { generator(self, stream); } stream.pop_node(); }); }; function PARENS(nodetype, func) { nodetype.DEFMETHOD("needs_parens", func); }; /* -----[ PARENTHESES ]----- */ PARENS(AST_Node, function(){ return false; }); // a function expression needs parens around it when it's provably // the first token to appear in a statement. PARENS(AST_Function, function(output){ return first_in_statement(output); }); // same goes for an object literal, because otherwise it would be // interpreted as a block of code. PARENS(AST_Object, function(output){ return first_in_statement(output); }); PARENS(AST_Seq, function(output){ var p = output.parent(); return p instanceof AST_Call // (foo, bar)() —or— foo(1, (2, 3), 4) || p instanceof AST_Binary // 1 + (2, 3) + 4 → 7 || p instanceof AST_VarDef // var a = (1, 2), b = a + a; → b = 4 || p instanceof AST_Dot // (1, {foo:2}).foo → 2 || p instanceof AST_Array // [ 1, (2, 3), 4 ] → [ 1, 3, 4 ] || p instanceof AST_ObjectProperty // { foo: (1, 2) }.foo → 2 || p instanceof AST_Conditional /* (false, true) ? (a = 10, b = 20) : (c = 30) * → 20 (side effect, set a = 10 and b = 20) */ ; }); PARENS(AST_Binary, function(output){ var p = output.parent(); // (foo && bar)() if (p instanceof AST_Call && p.expression === this) return true; // typeof (foo && bar) if (p instanceof AST_Unary) return true; // (foo && bar)["prop"], (foo && bar).prop if (p instanceof AST_PropAccess && p.expression === this) return true; // this deals with precedence: 3 * (2 + 1) if (p instanceof AST_Binary) { var po = p.operator, pp = PRECEDENCE[po]; var so = this.operator, sp = PRECEDENCE[so]; if (pp > sp || (pp == sp && this === p.right && !(so == po && (so == "*" || so == "&&" || so == "||")))) { return true; } } // for (var i = (foo in bar);;); ← perhaps useless, but valid syntax if (this.operator == "in") { // the “NoIn” stuff :-\ // UglifyJS 1.3.3 misses this one. if ((p instanceof AST_For || p instanceof AST_ForIn) && p.init === this) return true; if (p instanceof AST_VarDef) { var v = output.parent(1), p2 = output.parent(2); if ((p2 instanceof AST_For || p2 instanceof AST_ForIn) && p2.init === v) return true; } } }); PARENS(AST_New, function(output){ var p = output.parent(); // (new Date).getTime(); if (p instanceof AST_Dot && no_constructor_parens(this, output)) return true; }); function assign_and_conditional_paren_rules(output) { var p = output.parent(); // !(a = false) → true if (p instanceof AST_Unary) return true; // 1 + (a = 2) + 3 → 3, side effect setting a = 2 if (p instanceof AST_Binary && !(p instanceof AST_Assign)) return true; // (a = func)() —or— new (a = Object)() if (p instanceof AST_Call && p.expression === this) return true; // (a = foo) ? bar : baz if (p instanceof AST_Conditional && p.condition === this) return true; // (a = foo)["prop"] —or— (a = foo).prop if (p instanceof AST_PropAccess && p.expression === this) return true; }; PARENS(AST_Assign, assign_and_conditional_paren_rules); PARENS(AST_Conditional, assign_and_conditional_paren_rules); /* -----[ PRINTERS ]----- */ DEFPRINT(AST_Directive, function(self, output){ output.print_string(self.value); }); DEFPRINT(AST_Debugger, function(self, output){ output.print("debugger"); output.semicolon(); }); /* -----[ statements ]----- */ function display_body(body, is_toplevel, output) { var last = body.length - 1; body.forEach(function(stmt, i){ if (!(stmt instanceof AST_EmptyStatement)) { output.indent(); stmt.print(output); if (!(i == last && is_toplevel)) { output.newline(); if (is_toplevel) output.newline(); } } }); }; DEFPRINT(AST_Statement, function(self, output){ self.body.print(output); output.semicolon(); }); DEFPRINT(AST_Toplevel, function(self, output){ display_body(self.body, true, output); }); DEFPRINT(AST_LabeledStatement, function(self, output){ self.label.print(output); output.colon(); self.statement.print(output); }); DEFPRINT(AST_SimpleStatement, function(self, output){ self.body.print(output); output.semicolon(); }); DEFPRINT(AST_BlockStatement, function(self, output){ var body = self.body; //if (!(body instanceof Array)) body = [ body ]; if (body.length > 0) output.with_block(function(){ display_body(body, false, output); }); else output.print("{}"); }); DEFPRINT(AST_EmptyStatement, function(self, output){ output.semicolon(); }); DEFPRINT(AST_Do, function(self, output){ output.print("do"); output.space(); self.body.print(output); output.space(); output.print("while"); output.space(); output.with_parens(function(){ self.condition.print(output); }); output.semicolon(); }); DEFPRINT(AST_While, function(self, output){ output.print("while"); output.space(); output.with_parens(function(){ self.condition.print(output); }); output.space(); self.body.print(output); }); DEFPRINT(AST_For, function(self, output){ output.print("for"); output.space(); output.with_parens(function(){ if (self.init) { self.init.print(output); output.print(";"); output.space(); } else { output.print(";"); } if (self.condition) { self.condition.print(output); output.print(";"); output.space(); } else { output.print(";"); } if (self.step) { self.step.print(output); } }); output.space(); self.body.print(output); }); DEFPRINT(AST_ForIn, function(self, output){ output.print("for"); output.space(); output.with_parens(function(){ if (self.init) { self.init.print(output); } else { self.name.print(output); } output.space(); output.print("in"); output.space(); self.object.print(output); }); output.space(); self.body.print(output); }); DEFPRINT(AST_With, function(self, output){ output.print("with"); output.space(); output.with_parens(function(){ self.expression.print(output); }); output.space(); self.body.print(output); }); /* -----[ functions ]----- */ AST_Lambda.DEFMETHOD("_do_print", function(output, nokeyword){ var self = this; if (!nokeyword) { output.print("function"); } if (self.name) { output.space(); self.name.print(output); } output.with_parens(function(){ self.argnames.forEach(function(arg, i){ if (i) output.comma(); arg.print(output); }); }); output.space(); self.body.print(output); }); DEFPRINT(AST_Lambda, function(self, output){ self._do_print(output); }); /* -----[ exits ]----- */ AST_Exit.DEFMETHOD("_do_print", function(output, kind){ output.print(kind); if (this.value) { output.space(); this.value.print(output); } output.semicolon(); }); DEFPRINT(AST_Return, function(self, output){ self._do_print(output, "return"); }); DEFPRINT(AST_Throw, function(self, output){ self._do_print(output, "throw"); }); /* -----[ loop control ]----- */ AST_LoopControl.DEFMETHOD("_do_print", function(output, kind){ output.print(kind); if (this.label) { output.space(); this.label.print(output); } output.semicolon(); }); DEFPRINT(AST_Break, function(self, output){ self._do_print(output, "break"); }); DEFPRINT(AST_Continue, function(self, output){ self._do_print(output, "continue"); }); /* -----[ if ]----- */ function make_then(self, output) { // The squeezer replaces "block"-s that contain only a single // statement with the statement itself; technically, the AST // is correct, but this can create problems when we output an // IF having an ELSE clause where the THEN clause ends in an // IF *without* an ELSE block (then the outer ELSE would refer // to the inner IF). This function checks for this case and // adds the block brackets if needed. if (!self.consequent) return output.semicolon(); if (self.consequent instanceof AST_Do) { // https://github.com/mishoo/UglifyJS/issues/#issue/57 IE // croaks with "syntax error" on code like this: if (foo) // do ... while(cond); else ... we need block brackets // around do/while make_block(self.consequent, output); return; } var b = self.consequent; while (true) { if (b instanceof AST_If) { if (!b.alternative) { make_block(self.consequent, output); return; } b = b.alternative; } else if (b instanceof AST_While || b instanceof AST_Do || b instanceof AST_For || b instanceof AST_ForIn) { b = b.body; } else break; } self.consequent.print(output); }; DEFPRINT(AST_If, function(self, output){ output.print("if"); output.space(); output.with_parens(function(){ self.condition.print(output); }); output.space(); if (self.alternative) { make_then(self, output); output.space(); output.print("else"); output.space(); self.alternative.print(output); } else { self.consequent.print(output); } }); /* -----[ switch ]----- */ DEFPRINT(AST_Switch, function(self, output){ output.print("switch"); output.space(); output.with_parens(function(){ self.expression.print(output); }); output.space(); self.body.print(output); }); DEFPRINT(AST_SwitchBlock, function(self, output){ if (self.body.length > 0) output.with_block(function(){ self.body.forEach(function(stmt, i){ if (i) output.newline(); output.indent(true); stmt.print(output); }); }); else output.print("{}"); }); AST_SwitchBranch.DEFMETHOD("_do_print_body", function(output){ if (this.body.length > 0) { output.newline(); this.body.forEach(function(stmt){ output.indent(); stmt.print(output); output.newline(); }); } }); DEFPRINT(AST_Default, function(self, output){ output.print("default:"); self._do_print_body(output); }); DEFPRINT(AST_Case, function(self, output){ output.print("case"); output.space(); self.expression.print(output); output.print(":"); self._do_print_body(output); }); /* -----[ exceptions ]----- */ DEFPRINT(AST_Try, function(self, output){ output.print("try"); output.space(); self.btry.print(output); if (self.bcatch) { output.space(); self.bcatch.print(output); } if (self.bfinally) { output.space(); self.bfinally.print(output); } }); DEFPRINT(AST_Catch, function(self, output){ output.print("catch"); output.space(); output.with_parens(function(){ self.argname.print(output); }); output.space(); self.body.print(output); }); DEFPRINT(AST_Finally, function(self, output){ output.print("finally"); output.space(); self.body.print(output); }); /* -----[ var/const ]----- */ AST_Definitions.DEFMETHOD("_do_print", function(output, kind){ output.print(kind); output.space(); this.definitions.forEach(function(def, i){ if (i) output.comma(); def.print(output); }); var p = output.parent(); var in_for = p instanceof AST_For || p instanceof AST_ForIn; var avoid_semicolon = in_for && p.init === this; if (!avoid_semicolon) output.semicolon(); }); DEFPRINT(AST_Var, function(self, output){ self._do_print(output, "var"); }); DEFPRINT(AST_Const, function(self, output){ self._do_print(output, "const"); }); DEFPRINT(AST_VarDef, function(self, output){ self.name.print(output); if (self.value) { output.space(); output.print("="); output.space(); self.value.print(output); } }); /* -----[ other expressions ]----- */ DEFPRINT(AST_Call, function(self, output){ self.expression.print(output); if (self instanceof AST_New && no_constructor_parens(self, output)) return; output.with_parens(function(){ self.args.forEach(function(expr, i){ if (i) output.comma(); expr.print(output); }); }); }); DEFPRINT(AST_New, function(self, output){ output.print("new"); output.space(); AST_Call.prototype.print.call(self, output); }); DEFPRINT(AST_Seq, function(self, output){ self.first.print(output); output.comma(); self.second.print(output); }); DEFPRINT(AST_Dot, function(self, output){ var expr = self.expression; expr.print(output); if (expr instanceof AST_Number) { if (!/[xa-f.]/i.test(output.last())) { output.print("."); } } output.print("."); output.print_name(self.property); }); DEFPRINT(AST_Sub, function(self, output){ self.expression.print(output); output.print("["); self.property.print(output); output.print("]"); }); DEFPRINT(AST_UnaryPrefix, function(self, output){ var op = self.operator; output.print(op); if (/^[a-z]/i.test(op)) output.space(); self.expression.print(output); }); DEFPRINT(AST_UnaryPostfix, function(self, output){ self.expression.print(output); output.print(self.operator); }); DEFPRINT(AST_Binary, function(self, output){ self.left.print(output); output.space(); output.print(self.operator); output.space(); self.right.print(output); }); DEFPRINT(AST_Conditional, function(self, output){ self.condition.print(output); output.space(); output.print("?"); output.space(); self.consequent.print(output); output.space(); output.colon(); self.alternative.print(output); }); /* -----[ literals ]----- */ DEFPRINT(AST_Array, function(self, output){ output.with_square(function(){ var a = self.elements, len = a.length; if (len > 0) output.space(); a.forEach(function(exp, i){ if (i) output.comma(); exp.print(output); }); if (len > 0) output.space(); }); }); DEFPRINT(AST_Object, function(self, output){ if (self.properties.length > 0) output.with_block(function(){ self.properties.forEach(function(prop, i){ if (i) { output.print(","); output.newline(); } output.indent(); prop.print(output); }); output.newline(); }); else output.print("{}"); }); DEFPRINT(AST_ObjectKeyVal, function(self, output){ var key = self.key; if (output.options("quote_keys")) { output.print_string(key); } else if ((typeof key == "number" || !output.options("beautify") && +key + "" == key) && parseFloat(key) >= 0) { output.print(make_num(key)); } else if (!is_identifier(key)) { output.print_string(key); } else { output.print_name(key); } output.colon(); self.value.print(output); }); DEFPRINT(AST_ObjectSetter, function(self, output){ output.print("set"); output.space(); self.value._do_print(output, true); }); DEFPRINT(AST_ObjectGetter, function(self, output){ output.print("get"); output.space(); self.value._do_print(output, true); }); DEFPRINT(AST_Symbol, function(self, output){ output.print_name(self.name); }); DEFPRINT(AST_SymbolDeclaration, function(self, output){ if (self.uniq) { self.uniq.print(output); } else { output.print_name(self.mangled_name || self.name); } }); DEFPRINT(AST_SymbolRef, function(self, output){ var def = self.symbol; if (def) { def.print(output); } else { output.print_name(self.name); } }); DEFPRINT(AST_This, function(self, output){ output.print("this"); }); DEFPRINT(AST_Constant, function(self, output){ output.print(self.getValue()); }); DEFPRINT(AST_String, function(self, output){ output.print_string(self.getValue()); }); DEFPRINT(AST_Number, function(self, output){ output.print(make_num(self.getValue())); }); DEFPRINT(AST_RegExp, function(self, output){ output.print("/"); output.print(self.pattern); output.print("/"); if (self.mods) output.print(self.mods); }); // return true if the node at the top of the stack (that means the // innermost node in the current output) is lexically the first in // a statement. function first_in_statement(output) { var a = output.stack(), i = a.length, node = a[--i], p = a[--i]; while (i > 0) { if (p instanceof AST_Statement) return true; if ((p instanceof AST_Seq && p.first === node ) || (p instanceof AST_Call && p.expression === node ) || (p instanceof AST_Dot && p.expression === node ) || (p instanceof AST_Sub && p.expression === node ) || (p instanceof AST_Conditional && p.condition === node ) || (p instanceof AST_Binary && p.first === node ) || (p instanceof AST_Assign && p.first === node ) || (p instanceof AST_UnaryPostfix && p.expression === node )) { node = p; p = a[--i]; } else { return false; } } }; // self should be AST_New. decide if we want to show parens or not. function no_constructor_parens(self, output) { return self.args.length == 0 && !output.options("beautify"); }; function best_of(a) { var best = a[0], len = best.length; for (var i = 1; i < a.length; ++i) { if (a[i].length < len) { best = a[i]; len = best.length; } } return best; }; function make_num(num) { var str = num.toString(10), a = [ str.replace(/^0\./, ".").replace('e+', 'e') ], m; if (Math.floor(num) === num) { if (num >= 0) { a.push("0x" + num.toString(16).toLowerCase(), // probably pointless "0" + num.toString(8)); // same. } else { a.push("-0x" + (-num).toString(16).toLowerCase(), // probably pointless "-0" + (-num).toString(8)); // same. } if ((m = /^(.*?)(0+)$/.exec(num))) { a.push(m[1] + "e" + m[2].length); } } else if ((m = /^0?\.(0+)(.*)$/.exec(num))) { a.push(m[2] + "e-" + (m[1].length + m[2].length), str.substr(str.indexOf("."))); } return best_of(a); }; function make_block(stmt, output) { if (stmt instanceof AST_BlockStatement) { stmt.print(output); return; } output.with_block(function(){ output.indent(); stmt.print(output); output.newline(); }); }; })();