diff options
author | 2021-11-04 15:27:29 -0700 | |
---|---|---|
committer | 2021-11-04 15:27:29 -0700 | |
commit | fd57e2d9a630a2ba0d229419e11f39abd97f88bf (patch) | |
tree | e090f0cc333175e3852487bf2e360a44a1ceabfa | |
parent | 303a5ea898cd1df71a00caabd326c74940b379fc (diff) | |
download | bun-fd57e2d9a630a2ba0d229419e11f39abd97f88bf.tar.gz bun-fd57e2d9a630a2ba0d229419e11f39abd97f88bf.tar.zst bun-fd57e2d9a630a2ba0d229419e11f39abd97f88bf.zip |
[JSX] Match esbuild behavior for multiline JSX string literals
Diffstat (limited to '')
-rw-r--r-- | integration/snapshots/string-escapes.debug.js | 8 | ||||
-rw-r--r-- | integration/snapshots/string-escapes.hmr.debug.js | 8 | ||||
-rw-r--r-- | integration/snippets/jsx-spacing.jsx | 60 | ||||
-rw-r--r-- | src/js_lexer.zig | 79 | ||||
-rw-r--r-- | src/js_printer.zig | 20 |
5 files changed, 107 insertions, 68 deletions
diff --git a/integration/snapshots/string-escapes.debug.js b/integration/snapshots/string-escapes.debug.js index 4a5c82b05..af9c53abf 100644 --- a/integration/snapshots/string-escapes.debug.js +++ b/integration/snapshots/string-escapes.debug.js @@ -337,10 +337,10 @@ const jsxVariants = jsx(JSXFrag, { data: "\v" }, undefined, false, undefined, this), jsx("div", { - data: "\u2028" + data: "\\u2028" }, undefined, false, undefined, this), jsx("div", { - data: "\u2029" + data: "\\u2029" }, undefined, false, undefined, this), jsx("div", { data: "😊" @@ -392,11 +392,11 @@ const jsxVariants = jsx(JSXFrag, { jsx("div", { children: "\v" }, undefined, false, undefined, this), - jsx("div", {}, "\u2028", false, undefined, this), + jsx("div", {}, "\\u2028", false, undefined, this), jsx("div", { children: "\u2028" }, undefined, false, undefined, this), - jsx("div", {}, "\u2029", false, undefined, this), + jsx("div", {}, "\\u2029", false, undefined, this), jsx("div", { children: "\u2029" }, undefined, false, undefined, this), diff --git a/integration/snapshots/string-escapes.hmr.debug.js b/integration/snapshots/string-escapes.hmr.debug.js index 7a2d88ad1..19045846a 100644 --- a/integration/snapshots/string-escapes.hmr.debug.js +++ b/integration/snapshots/string-escapes.hmr.debug.js @@ -346,10 +346,10 @@ var hmr = new HMR(2482749838, "string-escapes.js"), exports = hmr.exports; data: "\v" }, undefined, false, undefined, this), jsx("div", { - data: "\u2028" + data: "\\u2028" }, undefined, false, undefined, this), jsx("div", { - data: "\u2029" + data: "\\u2029" }, undefined, false, undefined, this), jsx("div", { data: "😊" @@ -401,11 +401,11 @@ var hmr = new HMR(2482749838, "string-escapes.js"), exports = hmr.exports; jsx("div", { children: "\v" }, undefined, false, undefined, this), - jsx("div", {}, "\u2028", false, undefined, this), + jsx("div", {}, "\\u2028", false, undefined, this), jsx("div", { children: "\u2028" }, undefined, false, undefined, this), - jsx("div", {}, "\u2029", false, undefined, this), + jsx("div", {}, "\\u2029", false, undefined, this), jsx("div", { children: "\u2029" }, undefined, false, undefined, this), diff --git a/integration/snippets/jsx-spacing.jsx b/integration/snippets/jsx-spacing.jsx index 73e31ebaa..5feab1830 100644 --- a/integration/snippets/jsx-spacing.jsx +++ b/integration/snippets/jsx-spacing.jsx @@ -1,36 +1,40 @@ import * as ReactDOM from "react-dom/server"; -const Tester = ({ description }) => { - console.assert( - description === - "foo\nbar \n\nbaz\n\nthis\ntest\n\nchecks\nnewlines\nare\ngood\nyeah\n\n", - "Expected description to be 'foo\\nbar \\n\\nbaz\\n\\nthis\\ntest\\n\\nchecks\\nnewlines\\nare\\ngood\\nyeah\\n\\n' but was '" + - description + - "'" +const ReturnDescriptionAsString = ({ description }) => description; + +export function test() { + const _bun = ReactDOM.renderToString( + <ReturnDescriptionAsString + description="line1 +line2 trailing space + +line4 no trailing space 'single quote' \t\f\v\uF000 `template string` + +line6 no trailing space +line7 trailing newline that ${terminates} the string literal +" + ></ReturnDescriptionAsString> ); - return description; -}; + // convert HTML entities to unicode + const el = document.createElement("textarea"); + el.innerHTML = _bun; + const bun = el.value; -export function test() { - const foo = ReactDOM.renderToString( - <Tester - description="foo - bar - - baz - - this - test - - checks - newlines - are - good - yeah - - " - ></Tester> + const esbuild = + "line1\nline2 trailing space \n\nline4 no trailing space 'single quote' \\t\\f\\v\\uF000 `template string`\n\nline6 no trailing space\nline7 trailing newline that ${terminates} the string literal\n"; + + console.assert( + bun === esbuild, + `strings did not match: ${JSON.stringify( + { + received: bun, + expected: esbuild, + }, + null, + 2 + )}` ); + testDone(import.meta.url); } diff --git a/src/js_lexer.zig b/src/js_lexer.zig index 505a45869..d378ea670 100644 --- a/src/js_lexer.zig +++ b/src/js_lexer.zig @@ -1986,11 +1986,22 @@ pub fn NewLexer(comptime json_options: JSONOptions) type { needs_decode = true; try lexer.step(); }, + '\\' => { backslash = logger.Range{ .loc = logger.Loc{ .start = @intCast(i32, lexer.end), }, .len = 1 }; try lexer.step(); + + // JSX string literals do not support escaping + // They're "pre" escaped + switch (lexer.code_point) { + 'u', 0x0C, 0, '\t', std.ascii.control_code.VT, 0x08 => { + needs_decode = true; + }, + else => {}, + } + continue; }, quote => { @@ -2001,6 +2012,7 @@ pub fn NewLexer(comptime json_options: JSONOptions) type { try lexer.step(); break :string_literal; }, + else => { // Non-ASCII strings need the slow path if (lexer.code_point >= 0x80) { @@ -2158,43 +2170,46 @@ pub fn NewLexer(comptime json_options: JSONOptions) type { return decoded.items; } + inline fn maybeDecodeJSXEntity(lexer: *LexerType, text: string, out: *std.ArrayList(u16), cursor: *strings.CodepointIterator.Cursor) void { + if (strings.indexOfChar(text[cursor.width + cursor.i ..], ';')) |length| { + const end = cursor.width + cursor.i; + const entity = text[end .. end + length]; + if (entity[0] == '#') { + var number = entity[1..entity.len]; + var base: u8 = 10; + if (number.len > 1 and number[0] == 'x') { + number = number[1..number.len]; + base = 16; + } + + cursor.c = std.fmt.parseInt(i32, number, base) catch |err| brk: { + switch (err) { + error.InvalidCharacter => { + lexer.addError(lexer.start, "Invalid JSX entity escape: {s}", .{entity}, false); + }, + error.Overflow => { + lexer.addError(lexer.start, "JSX entity escape is too big: {s}", .{entity}, false); + }, + } + + break :brk strings.unicode_replacement; + }; + + cursor.i += @intCast(u32, length); + cursor.width = 1; + } else if (tables.jsxEntity.get(entity)) |ent| { + cursor.c = ent; + cursor.i += @intCast(u32, length) + 1; + } + } + } + pub fn decodeJSXEntities(lexer: *LexerType, text: string, out: *std.ArrayList(u16)) !void { const iterator = strings.CodepointIterator.init(text); var cursor = strings.CodepointIterator.Cursor{}; while (iterator.next(&cursor)) { - if (cursor.c == '&') { - if (strings.indexOfChar(text[cursor.width + cursor.i ..], ';')) |length| { - const end = cursor.width + cursor.i; - const entity = text[end .. end + length]; - if (entity[0] == '#') { - var number = entity[1..entity.len]; - var base: u8 = 10; - if (number.len > 1 and number[0] == 'x') { - number = number[1..number.len]; - base = 16; - } - cursor.c = std.fmt.parseInt(i32, number, base) catch |err| brk: { - switch (err) { - error.InvalidCharacter => { - lexer.addError(lexer.start, "Invalid JSX entity escape: {s}", .{entity}, false); - }, - error.Overflow => { - lexer.addError(lexer.start, "JSX entity escape is too big: {s}", .{entity}, false); - }, - } - - break :brk strings.unicode_replacement; - }; - - cursor.i += @intCast(u32, length); - cursor.width = 1; - } else if (tables.jsxEntity.get(entity)) |ent| { - cursor.c = ent; - cursor.i += @intCast(u32, length) + 1; - } - } - } + if (cursor.c == '&') lexer.maybeDecodeJSXEntity(text, out, &cursor); if (cursor.c <= 0xFFFF) { try out.append(@intCast(u16, cursor.c)); diff --git a/src/js_printer.zig b/src/js_printer.zig index f6468966e..d6be0aeb7 100644 --- a/src/js_printer.zig +++ b/src/js_printer.zig @@ -556,6 +556,14 @@ pub fn NewPrinter( '`' => { backtick_cost += 1; }, + '\r', '\n' => { + if (allow_backtick) { + return '`'; + } + }, + '\\' => { + i += 1; + }, '$' => { if (i + 1 < str.len and str[i + 1] == '{') { backtick_cost += 1; @@ -1836,6 +1844,18 @@ pub fn NewPrinter( '\\' => { i += 1; }, + // We must escape here for JSX string literals that contain unescaped newlines + // Those will get transformed into a template string + // which can potentially have unescaped $ + '$' => { + if (comptime c == '`') { + p.print(utf8[0..i]); + p.print("\\$"); + + utf8 = utf8[i + 1 ..]; + i = 0; + } + }, c => { p.print(utf8[0..i]); p.print("\\" ++ &[_]u8{c}); |