From fbbaa42ee55a7f753f7cab9b1a905ccf73cf26d5 Mon Sep 17 00:00:00 2001 From: Mihai Bazon Date: Tue, 27 Jan 2015 22:26:27 +0200 Subject: [PATCH] Add option to preserve/enforce string quote style `-q 0` (default) use single or double quotes such as to minimize the number of bytes (prefers double quotes when both will do); this is the previous behavior. `-q 1` -- always use single quotes `-q 2` -- always use double quotes `-q 3` or just `-q` -- always use the original quotes. Related codegen option: `quote_style`. Close #495 Close #460 Some `yargs` guru please tell me why `uglifyjs --help` doesn't display the help string for `-q` / `--quotes`, and why it doesn't output the expected argument types anymore, like good old `optimist` did. --- README.md | 7 +++++++ bin/uglifyjs | 11 +++++++++-- lib/ast.js | 15 ++++++++++----- lib/output.js | 34 +++++++++++++++++++++++++--------- lib/parse.js | 26 ++++++++++++++++++++------ 5 files changed, 71 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index d2ec432f..2bb21385 100644 --- a/README.md +++ b/README.md @@ -352,6 +352,13 @@ can pass additional arguments that control the code output: it will be prepended to the output literally. The source map will adjust for this text. Can be used to insert a comment containing licensing information, for example. +- `quote_style` (default `0`) -- preferred quote style for strings (affects + quoted property names and directives as well): + - `0` -- prefers double quotes, switches to single quotes when there are + more double quotes in the string itself. + - `1` -- always use single quotes + - `2` -- always use double quotes + - `3` -- always use the original quotes ### Keeping copyright notices or other comments diff --git a/bin/uglifyjs b/bin/uglifyjs index 11c3a01a..c9f4c12d 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -66,6 +66,7 @@ You need to pass an argument to this option to specify the name that your module .describe("noerr", "Don't throw an error for unknown options in -c, -b or -m.") .describe("bare-returns", "Allow return outside of functions. Useful when minifying CommonJS modules.") .describe("keep-fnames", "Do not mangle/drop function names. Useful for code relying on Function.prototype.name.") + .describe("quotes", "Quote style (0 - auto, 1 - single, 2 - double, 3 - original)") .alias("p", "prefix") .alias("o", "output") @@ -77,6 +78,7 @@ You need to pass an argument to this option to specify the name that your module .alias("r", "reserved") .alias("V", "version") .alias("e", "enclose") + .alias("q", "quotes") .string("source-map") .string("source-map-root") @@ -151,9 +153,14 @@ if (ARGS.r) { if (MANGLE) MANGLE.except = ARGS.r.replace(/^\s+|\s+$/g).split(/\s*,+\s*/); } +if (ARGS.quotes === true) { + ARGS.quotes = 3; +} + var OUTPUT_OPTIONS = { - beautify: BEAUTIFY ? true : false, - preamble: ARGS.preamble || null, + beautify : BEAUTIFY ? true : false, + preamble : ARGS.preamble || null, + quote_style : ARGS.quotes != null ? ARGS.quotes : 0 }; if (ARGS.screw_ie8) { diff --git a/lib/ast.js b/lib/ast.js index 5aa1be30..2e539cff 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -120,11 +120,12 @@ var AST_Debugger = DEFNODE("Debugger", null, { $documentation: "Represents a debugger statement", }, AST_Statement); -var AST_Directive = DEFNODE("Directive", "value scope", { +var AST_Directive = DEFNODE("Directive", "value scope quote", { $documentation: "Represents a directive, like \"use strict\";", $propdoc: { value: "[string] The value of this directive as a plain string (it's not an AST_String!)", - scope: "[AST_Scope/S] The scope that this directive affects" + scope: "[AST_Scope/S] The scope that this directive affects", + quote: "[string] the original quote character" }, }, AST_Statement); @@ -766,8 +767,11 @@ var AST_ObjectProperty = DEFNODE("ObjectProperty", "key value", { } }); -var AST_ObjectKeyVal = DEFNODE("ObjectKeyVal", null, { +var AST_ObjectKeyVal = DEFNODE("ObjectKeyVal", "quote", { $documentation: "A key: value object property", + $propdoc: { + quote: "[string] the original quote character" + } }, AST_ObjectProperty); var AST_ObjectSetter = DEFNODE("ObjectSetter", null, { @@ -852,10 +856,11 @@ var AST_Constant = DEFNODE("Constant", null, { } }); -var AST_String = DEFNODE("String", "value", { +var AST_String = DEFNODE("String", "value quote", { $documentation: "A string literal", $propdoc: { - value: "[string] the contents of this string" + value: "[string] the contents of this string", + quote: "[string] the original quote character" } }, AST_Constant); diff --git a/lib/output.js b/lib/output.js index bfe6242a..135636b9 100644 --- a/lib/output.js +++ b/lib/output.js @@ -63,6 +63,7 @@ function OutputStream(options) { preserve_line : false, screw_ie8 : false, preamble : null, + quote_style : 0 }, true); var indentation = 0; @@ -84,7 +85,7 @@ function OutputStream(options) { }); }; - function make_string(str) { + function make_string(str, quote) { var dq = 0, sq = 0; str = str.replace(/[\\\b\f\n\r\t\x22\x27\u2028\u2029\0\ufeff]/g, function(s){ switch (s) { @@ -102,13 +103,27 @@ function OutputStream(options) { } return s; }); + function quote_single() { + return "'" + str.replace(/\x27/g, "\\'") + "'"; + } + function quote_double() { + return '"' + str.replace(/\x22/g, '\\"') + '"'; + } if (options.ascii_only) str = to_ascii(str); - if (dq > sq) return "'" + str.replace(/\x27/g, "\\'") + "'"; - else return '"' + str.replace(/\x22/g, '\\"') + '"'; + switch (options.quote_style) { + case 1: + return quote_single(); + case 2: + return quote_double(); + case 3: + return quote == "'" ? quote_single() : quote_double(); + default: + return dq > sq ? quote_single() : quote_double(); + } }; - function encode_string(str) { - var ret = make_string(str); + function encode_string(str, quote) { + var ret = make_string(str, quote); if (options.inline_script) ret = ret.replace(/<\x2fscript([>\/\t\n\f\r ])/gi, "<\\/script$1"); return ret; @@ -324,7 +339,7 @@ function OutputStream(options) { force_semicolon : force_semicolon, to_ascii : to_ascii, print_name : function(name) { print(make_name(name)) }, - print_string : function(str) { print(encode_string(str)) }, + print_string : function(str, quote) { print(encode_string(str, quote)) }, next_indent : next_indent, with_indent : with_indent, with_block : with_block, @@ -581,7 +596,7 @@ function OutputStream(options) { /* -----[ PRINTERS ]----- */ DEFPRINT(AST_Directive, function(self, output){ - output.print_string(self.value); + output.print_string(self.value, self.quote); output.semicolon(); }); DEFPRINT(AST_Debugger, function(self, output){ @@ -1077,6 +1092,7 @@ function OutputStream(options) { }); DEFPRINT(AST_ObjectKeyVal, function(self, output){ var key = self.key; + var quote = self.quote; if (output.option("quote_keys")) { output.print_string(key + ""); } else if ((typeof key == "number" @@ -1087,7 +1103,7 @@ function OutputStream(options) { } else if (RESERVED_WORDS(key) ? output.option("screw_ie8") : is_identifier_string(key)) { output.print_name(key); } else { - output.print_string(key); + output.print_string(key, quote); } output.colon(); self.value.print(output); @@ -1125,7 +1141,7 @@ function OutputStream(options) { output.print(self.getValue()); }); DEFPRINT(AST_String, function(self, output){ - output.print_string(self.getValue()); + output.print_string(self.getValue(), self.quote); }); DEFPRINT(AST_Number, function(self, output){ output.print(make_num(self.getValue())); diff --git a/lib/parse.js b/lib/parse.js index 35ce7ef2..78c1dd41 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -367,7 +367,7 @@ function tokenizer($TEXT, filename, html5_comments) { return num; }; - var read_string = with_eof_error("Unterminated string constant", function(){ + var read_string = with_eof_error("Unterminated string constant", function(quote_char){ var quote = next(), ret = ""; for (;;) { var ch = next(true); @@ -392,7 +392,9 @@ function tokenizer($TEXT, filename, html5_comments) { else if (ch == quote) break; ret += ch; } - return token("string", ret); + var tok = token("string", ret); + tok.quote = quote_char; + return tok; }); function skip_line_comment(type) { @@ -547,7 +549,7 @@ function tokenizer($TEXT, filename, html5_comments) { if (!ch) return token("eof"); var code = ch.charCodeAt(0); switch (code) { - case 34: case 39: return read_string(); + case 34: case 39: return read_string(ch); case 46: return handle_dot(); case 47: return handle_slash(); } @@ -737,8 +739,14 @@ function parse($TEXT, options) { case "string": var dir = S.in_directives, stat = simple_statement(); // XXXv2: decide how to fix directives - if (dir && stat.body instanceof AST_String && !is("punc", ",")) - return new AST_Directive({ value: stat.body.value }); + if (dir && stat.body instanceof AST_String && !is("punc", ",")) { + return new AST_Directive({ + start : stat.body.start, + end : stat.body.end, + quote : stat.body.quote, + value : stat.body.value, + }); + } return stat; case "num": case "regexp": @@ -1124,7 +1132,12 @@ function parse($TEXT, options) { ret = new AST_Number({ start: tok, end: tok, value: tok.value }); break; case "string": - ret = new AST_String({ start: tok, end: tok, value: tok.value }); + ret = new AST_String({ + start : tok, + end : tok, + value : tok.value, + quote : tok.quote + }); break; case "regexp": ret = new AST_RegExp({ start: tok, end: tok, value: tok.value }); @@ -1237,6 +1250,7 @@ function parse($TEXT, options) { expect(":"); a.push(new AST_ObjectKeyVal({ start : start, + quote : start.quote, key : name, value : expression(false), end : prev() -- 2.34.1