Skip to content

Commit

Permalink
Nested template strings
Browse files Browse the repository at this point in the history
Fixes #797
  • Loading branch information
bitwiseman committed Jan 29, 2016
1 parent 94324e5 commit 530a3ad
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 86 deletions.
68 changes: 46 additions & 22 deletions js/lib/beautify.js
Original file line number Diff line number Diff line change
Expand Up @@ -1864,10 +1864,12 @@

}

var startXmlRegExp = /^<([-a-zA-Z:0-9_.]+|{.+?}|!\[CDATA\[[\s\S]*?\]\])(\s+{.+?}|\s+[-a-zA-Z:0-9_.]+|\s+[-a-zA-Z:0-9_.]+\s*=\s*('[^']*'|"[^"]*"|{.+?}))*\s*(\/?)\s*>/

if (c === '`' || c === "'" || c === '"' || // string
(
(c === '/') || // regexp
(opts.e4x && c === "<" && input.slice(parser_pos - 1).match(/^<([-a-zA-Z:0-9_.]+|{.+?}|!\[CDATA\[[\s\S]*?\]\])(\s+{.+?}|\s+[-a-zA-Z:0-9_.]+|\s+[-a-zA-Z:0-9_.]+\s*=\s*('[^']*'|"[^"]*"|{.+?}))*\s*(\/?)\s*>/)) // xml
(opts.e4x && c === "<" && input.slice(parser_pos - 1).match(startXmlRegExp)) // xml
) && ( // regex and xml can only appear in specific locations during parsing
(last_token.type === 'TK_RESERVED' && in_array(last_token.text , ['return', 'case', 'throw', 'else', 'do', 'typeof', 'yield'])) ||
(last_token.type === 'TK_END_EXPR' && last_token.text === ')' &&
Expand Down Expand Up @@ -1908,6 +1910,7 @@
//
// handle e4x xml literals
//

var xmlRegExp = /<(\/?)([-a-zA-Z:0-9_.]+|{.+?}|!\[CDATA\[[\s\S]*?\]\])(\s+{.+?}|\s+[-a-zA-Z:0-9_.]+|\s+[-a-zA-Z:0-9_.]+\s*=\s*('[^']*'|"[^"]*"|{.+?}))*\s*(\/?)\s*>/g;
var xmlStr = input.slice(parser_pos - 1);
var match = xmlRegExp.exec(xmlStr);
Expand Down Expand Up @@ -1940,31 +1943,52 @@
//
// handle string
//
// Template strings can travers lines without escape characters.
// Other strings cannot
while (parser_pos < input_length &&
(esc || (input.charAt(parser_pos) !== sep &&
(sep === '`' || !acorn.newline.test(input.charAt(parser_pos)))))) {
// Handle \r\n linebreaks after escapes or in template strings
if ((esc || sep === '`') && acorn.newline.test(input.charAt(parser_pos))) {
if (input.charAt(parser_pos) === '\r' && input.charAt(parser_pos + 1) === '\n') {
parser_pos += 1;
var parse_string = function(delimiter, allow_unescaped_newlines, start_sub) {
// Template strings can travers lines without escape characters.
// Other strings cannot
var current_char;
while (parser_pos < input_length) {
current_char = input.charAt(parser_pos);
if (!(esc || (current_char !== delimiter &&
(allow_unescaped_newlines || !acorn.newline.test(current_char))))) {
break;
}
resulting_string += '\n';
} else {
resulting_string += input.charAt(parser_pos);
}
if (esc) {
if (input.charAt(parser_pos) === 'x' || input.charAt(parser_pos) === 'u') {
has_char_escapes = true;

// Handle \r\n linebreaks after escapes or in template strings
if ((esc || allow_unescaped_newlines) && acorn.newline.test(current_char)) {
if (current_char === '\r' && input.charAt(parser_pos + 1) === '\n') {
parser_pos += 1;
current_char = input.charAt(parser_pos);
}
resulting_string += '\n';
} else {
resulting_string += current_char;
}
if (esc) {
if (current_char === 'x' || current_char === 'u') {
has_char_escapes = true;
}
esc = false;
} else {
esc = current_char === '\\';
}

parser_pos += 1;

if (start_sub && resulting_string.indexOf(start_sub, resulting_string.length - start_sub.length) !== -1) {
if (delimiter === '`') {
parse_string('}', allow_unescaped_newlines, '`')
} else {
parse_string('`', allow_unescaped_newlines, '${')
}
}
esc = false;
} else {
esc = input.charAt(parser_pos) === '\\';
}
parser_pos += 1;
}

if (sep === '`') {
parse_string('`', true, '${')
} else {
parse_string(sep)
}
}

if (has_char_escapes && opts.unescape_strings) {
Expand Down
18 changes: 10 additions & 8 deletions js/test/generated/beautify-javascript-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,16 @@ function run_javascript_tests(test_obj, Urlencoded, js_beautify, html_beautify,
'};');


reset_options();
//============================================================
// Test template and continuation strings
bt('`This is a ${template} string.`');
bt('`This\n is\n a\n ${template}\n string.`');
bt('a = `This is a continuation\\nstring.`');
bt('a = "This is a continuation\\nstring."');
bt('`SELECT\n nextval(\'${this.options.schema ? `${this.options.schema}.` : \'\'}"${this.tableName}_${this.autoIncrementField}_seq"\'::regclass\n ) nextval;`');


reset_options();
//============================================================
// ES7 Decorators
Expand Down Expand Up @@ -2766,14 +2776,6 @@ function run_javascript_tests(test_obj, Urlencoded, js_beautify, html_beautify,
bt('a= f[b];',
'a = f[ b ];');

reset_options();
//============================================================
// Test template strings
bt('`This is a ${template} string.`', '`This is a ${template} string.`');
bt('`This\n is\n a\n ${template}\n string.`', '`This\n is\n a\n ${template}\n string.`');
bt('a = `This is a continuation\\\nstring.`', 'a = `This is a continuation\\\nstring.`');
bt('a = "This is a continuation\\\nstring."', 'a = "This is a continuation\\\nstring."');

Urlencoded.run_tests(sanitytest);
}

Expand Down
83 changes: 49 additions & 34 deletions python/jsbeautifier/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1744,42 +1744,57 @@ def __tokenize_next(self):

else:
# handle string
while self.parser_pos < len(self.input) and \
(esc or (self.input[self.parser_pos] != sep and
(sep == '`' or not self.acorn.newline.match(self.input[self.parser_pos])))):
resulting_string += self.input[self.parser_pos]
# Handle \r\n linebreaks after escapes or in template strings
if self.input[self.parser_pos] == '\r' and self.parser_pos + 1 < len(self.input) and self.input[self.parser_pos + 1] == '\n':
def parse_string(self, resulting_string, delimiter, allow_unescaped_newlines = False, start_sub = None):
esc = False
esc1 = 0
esc2 = 0
while self.parser_pos < len(self.input) and \
(esc or (self.input[self.parser_pos] != delimiter and
(allow_unescaped_newlines or not self.acorn.newline.match(self.input[self.parser_pos])))):
resulting_string += self.input[self.parser_pos]
# Handle \r\n linebreaks after escapes or in template strings
if self.input[self.parser_pos] == '\r' and self.parser_pos + 1 < len(self.input) and self.input[self.parser_pos + 1] == '\n':
self.parser_pos += 1
resulting_string += '\n'

if esc1 and esc1 >= esc2:
try:
esc1 = int(resulting_string[-esc2:], 16)
except Exception:
esc1 = False
if esc1 and esc1 >= 0x20 and esc1 <= 0x7e:
esc1 = chr(esc1)
resulting_string = resulting_string[:-2 - esc2]
if esc1 == delimiter or esc1 == '\\':
resulting_string += '\\'
resulting_string += esc1
esc1 = 0
if esc1:
esc1 += 1
elif not esc:
esc = self.input[self.parser_pos] == '\\'
else:
esc = False
if self.opts.unescape_strings:
if self.input[self.parser_pos] == 'x':
esc1 += 1
esc2 = 2
elif self.input[self.parser_pos] == 'u':
esc1 += 1
esc2 = 4
self.parser_pos += 1
resulting_string += '\n'

if esc1 and esc1 >= esc2:
try:
esc1 = int(resulting_string[-esc2:], 16)
except Exception:
esc1 = False
if esc1 and esc1 >= 0x20 and esc1 <= 0x7e:
esc1 = chr(esc1)
resulting_string = resulting_string[:-2 - esc2]
if esc1 == sep or esc1 == '\\':
resulting_string += '\\'
resulting_string += esc1
esc1 = 0
if esc1:
esc1 += 1
elif not esc:
esc = self.input[self.parser_pos] == '\\'
else:
esc = False
if self.opts.unescape_strings:
if self.input[self.parser_pos] == 'x':
esc1 += 1
esc2 = 2
elif self.input[self.parser_pos] == 'u':
esc1 += 1
esc2 = 4
self.parser_pos += 1

if start_sub and resulting_string.endswith(start_sub):
if delimiter == '`':
resulting_string = parse_string(self, resulting_string, '}', allow_unescaped_newlines, '`')
else:
resulting_string = parse_string(self, resulting_string, '`', allow_unescaped_newlines, '${')
return resulting_string

if sep == '`':
resulting_string = parse_string(self, resulting_string, '`', True, '${')
else:
resulting_string = parse_string(self, resulting_string, sep)

if self.parser_pos < len(self.input) and self.input[self.parser_pos] == sep:
resulting_string += sep
Expand Down
17 changes: 10 additions & 7 deletions python/jsbeautifier/tests/generated/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ def unicode_char(value):
'};')


self.reset_options();
#============================================================
# Test template and continuation strings
bt('`This is a ${template} string.`')
bt('`This\n is\n a\n ${template}\n string.`')
bt('a = `This is a continuation\\nstring.`')
bt('a = "This is a continuation\\nstring."')
bt('`SELECT\n nextval(\'${this.options.schema ? `${this.options.schema}.` : \'\'}"${this.tableName}_${this.autoIncrementField}_seq"\'::regclass\n ) nextval;`')


self.reset_options();
#============================================================
# ES7 Decorators
Expand Down Expand Up @@ -2934,13 +2944,6 @@ def test_beautifier_unconverted(self):
bt('a= f[b];',
'a = f[ b ];')

self.reset_options();
#============================================================
# Test template strings
bt('`This is a ${template} string.`', '`This is a ${template} string.`')
bt('`This\n is\n a\n ${template}\n string.`', '`This\n is\n a\n ${template}\n string.`')
bt('a = `This is a continuation\\\nstring.`', 'a = `This is a continuation\\\nstring.`');
bt('a = "This is a continuation\\\nstring."', 'a = "This is a continuation\\\nstring."');

def decodesto(self, input, expectation=None):
if expectation == None:
Expand Down
8 changes: 0 additions & 8 deletions test/data/javascript/node.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -1095,14 +1095,6 @@ function run_javascript_tests(test_obj, Urlencoded, js_beautify, html_beautify,
bt('a= f[b];',
'a = f[ b ];');
reset_options();
//============================================================
// Test template strings
bt('`This is a ${template} string.`', '`This is a ${template} string.`');
bt('`This\n is\n a\n ${template}\n string.`', '`This\n is\n a\n ${template}\n string.`');
bt('a = `This is a continuation\\\nstring.`', 'a = `This is a continuation\\\nstring.`');
bt('a = "This is a continuation\\\nstring."', 'a = "This is a continuation\\\nstring."');
Urlencoded.run_tests(sanitytest);
}

Expand Down
7 changes: 0 additions & 7 deletions test/data/javascript/python.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -1263,13 +1263,6 @@ class TestJSBeautifier(unittest.TestCase):
bt('a= f[b];',
'a = f[ b ];')

self.reset_options();
#============================================================
# Test template strings
bt('`This is a ${template} string.`', '`This is a ${template} string.`')
bt('`This\n is\n a\n ${template}\n string.`', '`This\n is\n a\n ${template}\n string.`')
bt('a = `This is a continuation\\\nstring.`', 'a = `This is a continuation\\\nstring.`');
bt('a = "This is a continuation\\\nstring."', 'a = "This is a continuation\\\nstring."');

def decodesto(self, input, expectation=None):
if expectation == None:
Expand Down
11 changes: 11 additions & 0 deletions test/data/javascript/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ exports.test_data = {
"};"]
}
],

}, {
name: "Test template and continuation strings",
description: "",
tests: [
{ unchanged: '`This is a ${template} string.`' },
{ unchanged: '`This\n is\n a\n ${template}\n string.`' },
{ unchanged: 'a = `This is a continuation\\\nstring.`' },
{ unchanged: 'a = "This is a continuation\\\nstring."' },
{ unchanged: '`SELECT\n nextval(\\\'${this.options.schema ? `${this.options.schema}.` : \\\'\\\'}"${this.tableName}_${this.autoIncrementField}_seq"\\\'::regclass\n ) nextval;`' },
]
}, {
name: "ES7 Decorators",
description: "Permit ES7 decorators, which are invoked with a leading \"@\".",
Expand Down

0 comments on commit 530a3ad

Please sign in to comment.