Files
UglifyJS/lib/output.js

903 lines
28 KiB
JavaScript

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_This, function(self, output){
output.print("this");
});
DEFPRINT(AST_Label, function(self, output){
output.print_name(self.name);
});
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();
});
};
})();