diff --git a/lib/compress.js b/lib/compress.js index 399bfdcb..1cd6c773 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -4922,11 +4922,9 @@ merge(Compressor.prototype, { return this; function decode(str) { - return str.replace(/\\(u\{[^}]*\}?|u[\s\S]{0,4}|x[\s\S]{0,2}|[0-9]+|[\s\S])/g, function(match, seq) { - var s = decode_escape_sequence(seq); - if (typeof s != "string") malformed = true; - return s; - }); + str = decode_template(str); + if (typeof str != "string") malformed = true; + return str; } }); })(function(node, func) { @@ -11336,42 +11334,84 @@ merge(Compressor.prototype, { && tag.expression.name == "String"; } + function decode_template(str) { + var malformed = false; + str = str.replace(/\\(u\{[^}]*\}?|u[\s\S]{0,4}|x[\s\S]{0,2}|[0-9]+|[\s\S])/g, function(match, seq) { + var ch = decode_escape_sequence(seq); + if (typeof ch == "string") return ch; + malformed = true; + }); + if (!malformed) return str; + } + OPT(AST_Template, function(self, compressor) { if (!compressor.option("templates")) return self; var tag = self.tag; if (!tag || is_raw_tag(compressor, tag)) { - var exprs = self.expressions.slice(); - var strs = self.strings.slice(); - var CHANGED = false; - for (var i = exprs.length; --i >= 0;) { - var node = exprs[i]; - var ev = node.evaluate(compressor); - if (ev === node) continue; - if (tag && /\r|\\|`/.test(ev)) continue; - ev = ("" + ev).replace(/\r|\\|`/g, function(s) { - return "\\" + (s == "\r" ? "r" : s); - }); - if (ev.length > node.print_to_string().length + 3) continue; - var combined = strs[i] + ev + strs[i + 1]; - if (typeof make_node(AST_Template, self, { - expressions: [], - strings: [ combined ], - tag: tag, - }).evaluate(compressor) != typeof make_node(AST_Template, self, { - expressions: [ node ], - strings: strs.slice(i, i + 2), - tag: tag, - }).evaluate(compressor)) continue; - exprs.splice(i, 1); - strs.splice(i, 2, combined); - CHANGED = true; + var exprs = []; + var strs = []; + for (var i = 0, status; i < self.strings.length; i++) { + var str = self.strings[i]; + if (!tag) { + var trimmed = decode_template(str); + if (trimmed) str = escape_literal(trimmed); + } + if (i > 0) { + var node = self.expressions[i - 1]; + var value = should_join(node); + if (value) { + var prev = strs[strs.length - 1]; + var joined = prev + value + str; + var decoded; + if (tag || typeof (decoded = decode_template(joined)) == status) { + strs[strs.length - 1] = decoded ? escape_literal(decoded) : joined; + continue; + } + } + exprs.push(node); + } + strs.push(str); + if (!tag) status = typeof trimmed; } - if (CHANGED) { - self.expressions = exprs; - self.strings = strs; + if (!tag && strs.length > 1) { + if (strs[0] == "") return make_node(AST_Binary, self, { + operator: "+", + left: exprs[0], + right: make_node(AST_Template, self, { + expressions: exprs.slice(1), + strings: strs.slice(1), + tag: tag, + }).transform(compressor), + }).optimize(compressor); + if (strs[strs.length - 1] == "") return make_node(AST_Binary, self, { + operator: "+", + left: make_node(AST_Template, self, { + expressions: exprs.slice(0, -1), + strings: strs.slice(0, -1), + tag: tag, + }).transform(compressor), + right: exprs[exprs.length - 1], + }).optimize(compressor); } + self.expressions = exprs; + self.strings = strs; } return try_evaluate(compressor, self); + + function escape_literal(str) { + return str.replace(/\r|\\|`|\${/g, function(s) { + return "\\" + (s == "\r" ? "r" : s); + }); + } + + function should_join(node) { + var ev = node.evaluate(compressor); + if (ev === node) return; + if (tag && /\r|\\|`/.test(ev)) return; + ev = escape_literal("" + ev); + if (ev.length > node.print_to_string().length + "${}".length) return; + return ev; + } }); function is_atomic(lhs, self) { diff --git a/lib/parse.js b/lib/parse.js index a80fcaf9..16ac7ddb 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -242,10 +242,10 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { read_template : with_eof_error("Unterminated template literal", function(strings) { var s = ""; for (;;) { - var ch = next(true, true); + var ch = read(); switch (ch) { case "\\": - ch += next(true, true); + ch += read(); break; case "`": strings.push(s); @@ -260,6 +260,11 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { } s += ch; } + + function read() { + var ch = next(true, true); + return ch == "\r" ? "\n" : ch; + } }), }; var prev_was_dot = false; diff --git a/test/compress/templates.js b/test/compress/templates.js index 3c1661fe..81bc8610 100644 --- a/test/compress/templates.js +++ b/test/compress/templates.js @@ -131,6 +131,105 @@ malformed_escape: { node_version: ">=4" } +booleans: { + options = { + booleans: true, + evaluate: true, + templates: true, + } + input: { + var a; + console.log(`$${a}${a}` ? "PASS" : "FAIL"); + } + expect: { + var a; + console.log("$" + a + a ? "PASS" : "FAIL"); + } + expect_stdout: "PASS" + node_version: ">=4" +} + +escape_placeholder_1: { + options = { + templates: true, + } + input: { + console.log(`\${\n`); + } + expect: { + console.log(`\${ +`); + } + expect_stdout: [ + "${", + "", + ] + node_version: ">=4" +} + +escape_placeholder_2: { + options = { + evaluate: true, + templates: true, + } + input: { + console.log(`\n${"${"}\n`); + } + expect: { + console.log(` +\${ +`); + } + expect_stdout: [ + "", + "${", + "", + ] + node_version: ">=4" +} + +escape_placeholder_3: { + options = { + evaluate: true, + templates: true, + } + input: { + console.log(`\n$${"{"}\n`); + } + expect: { + console.log(` +\${ +`); + } + expect_stdout: [ + "", + "${", + "", + ] + node_version: ">=4" +} + +escape_placeholder_4: { + options = { + evaluate: true, + templates: true, + } + input: { + console.log(`\n${"$"}${"{"}\n`); + } + expect: { + console.log(` +\${ +`); + } + expect_stdout: [ + "", + "${", + "", + ] + node_version: ">=4" +} + evaluate: { options = { evaluate: true, @@ -174,7 +273,7 @@ partial_evaluate: { console.log(`${6 * 7} foo ${console ? `PA` + "SS" : `FA` + `IL`}`); } expect: { - console.log(`42 foo ${console ? "PASS" : "FAIL"}`); + console.log("42 foo " + (console ? "PASS" : "FAIL")); } expect_stdout: "42 foo PASS" node_version: ">=4" @@ -204,7 +303,7 @@ malformed_evaluate_2: { console.log(`\u0${0}b${5}`); } expect: { - console.log(`\u0${0}b5`); + console.log(`\u00b` + 5); } expect_stdout: true node_version: ">=4" @@ -357,13 +456,14 @@ issue_4604: { issue_4606: { options = { evaluate: true, + strings: true, templates: true, } input: { console.log(`${typeof A} ${"\r"} ${"\\"} ${"`"}`); } expect: { - console.log(`${typeof A} \r \\ \``); + console.log(typeof A + " \r \\ `"); } expect_stdout: "undefined \r \\ `" node_version: ">=4" @@ -434,3 +534,154 @@ issue_4931: { ] node_version: ">=4" } + +issue_5125_1: { + options = { + evaluate: true, + strings: true, + templates: true, + } + input: { + console.log(`PASS ${typeof A}`); + } + expect: { + console.log("PASS " + typeof A); + } + expect_stdout: "PASS undefined" + node_version: ">=4" +} + +issue_5125_2: { + options = { + evaluate: true, + strings: true, + templates: true, + } + input: { + console.log(`PASS +${typeof A}`); + } + expect: { + console.log(`PASS +` + typeof A); + } + expect_stdout: [ + "PASS", + "undefined", + ] + node_version: ">=4" +} + +issue_5125_3: { + options = { + evaluate: true, + strings: true, + templates: true, + } + input: { + console.log(`PASS\n${typeof A}`); + } + expect: { + console.log(`PASS +` + typeof A); + } + expect_stdout: [ + "PASS", + "undefined", + ] + node_version: ">=4" +} + +issue_5125_4: { + options = { + evaluate: true, + strings: true, + templates: true, + } + input: { + console.log(`PASS + +${typeof A}`); + } + expect: { + console.log(`PASS + +` + typeof A); + } + expect_stdout: [ + "PASS", + "", + "undefined", + ] + node_version: ">=4" +} + +issue_5125_5: { + options = { + evaluate: true, + strings: true, + templates: true, + } + input: { + console.log(`PASS\n\n${typeof A}`); + } + expect: { + console.log(`PASS + +` + typeof A); + } + expect_stdout: [ + "PASS", + "", + "undefined", + ] + node_version: ">=4" +} + +issue_5125_6: { + options = { + evaluate: true, + strings: true, + templates: true, + } + input: { + console.log(`${typeof A} ${typeof B} PASS`); + } + expect: { + console.log(typeof A + ` ${typeof B} PASS`); + } + expect_stdout: "undefined undefined PASS" + node_version: ">=4" +} + +issue_5125_7: { + options = { + evaluate: true, + strings: true, + templates: true, + } + input: { + console.log(`${typeof A} ${typeof B} ${typeof C} PASS`); + } + expect: { + console.log(typeof A + ` ${typeof B} ${typeof C} PASS`); + } + expect_stdout: "undefined undefined undefined PASS" + node_version: ">=4" +} + +issue_5125_8: { + options = { + evaluate: true, + strings: true, + templates: true, + } + input: { + console.log(`${typeof A}${typeof B}${typeof C} PASS`); + } + expect: { + console.log(typeof A + typeof B + typeof C + " PASS"); + } + expect_stdout: "undefinedundefinedundefined PASS" + node_version: ">=4" +} diff --git a/test/mocha/templates.js b/test/mocha/templates.js index 169e791e..3e5134a8 100644 --- a/test/mocha/templates.js +++ b/test/mocha/templates.js @@ -34,20 +34,20 @@ describe("Template literals", function() { [ // native line breaks [ "`foo\nbar`", "`foo\nbar`" ], - [ "`foo\rbar`", "`foo\rbar`" ], + [ "`foo\rbar`", "`foo\nbar`" ], [ "`foo\r\nbar`", "`foo\nbar`" ], - [ "`foo\r\n\rbar`", "`foo\n\rbar`" ], + [ "`foo\r\n\rbar`", "`foo\n\nbar`" ], // escaped line breaks [ "`foo\\nbar`", "`foo\\nbar`" ], [ "`foo\\rbar`", "`foo\\rbar`" ], - [ "`foo\r\\nbar`", "`foo\r\\nbar`" ], + [ "`foo\r\\nbar`", "`foo\n\\nbar`" ], [ "`foo\\r\nbar`", "`foo\\r\nbar`" ], [ "`foo\\r\\nbar`", "`foo\\r\\nbar`" ], // continuation [ "`foo\\\nbar`", "`foo\\\nbar`" ], - [ "`foo\\\rbar`", "`foo\\\rbar`" ], + [ "`foo\\\rbar`", "`foo\\\nbar`" ], [ "`foo\\\r\nbar`", "`foo\\\nbar`" ], - [ "`foo\\\r\n\rbar`", "`foo\\\n\rbar`" ], + [ "`foo\\\r\n\rbar`", "`foo\\\n\nbar`" ], [ "`foo\\\\nbar`", "`foo\\\\nbar`" ], [ "`foo\\\\rbar`", "`foo\\\\rbar`" ], [ "`foo\\\\r\nbar`", "`foo\\\\r\nbar`" ],