diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 680c446c0d5..0f96b04df9a 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -2030,7 +2030,7 @@ const _super = (function (geti, seti) { emitTrailingCommentsOfPosition(commentRange.pos); } - emitExpression(node.initializer); + emitExpression(initializer); } function emitShorthandPropertyAssignment(node: ShorthandPropertyAssignment) { diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index ac43e0fb63c..e14a60eb10c 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -905,6 +905,10 @@ namespace ts { return currentToken = scanner.scanJsxToken(); } + function scanJsxAttributeValue(): SyntaxKind { + return currentToken = scanner.scanJsxAttributeValue(); + } + function speculationHelper(callback: () => T, isLookAhead: boolean): T { // Keep track of the state we'll need to rollback to if lookahead fails (or if the // caller asked us to always reset our state). @@ -3831,8 +3835,8 @@ namespace ts { scanJsxIdentifier(); const node = createNode(SyntaxKind.JsxAttribute); node.name = parseIdentifierName(); - if (parseOptional(SyntaxKind.EqualsToken)) { - switch (token()) { + if (token() === SyntaxKind.EqualsToken) { + switch (scanJsxAttributeValue()) { case SyntaxKind.StringLiteral: node.initializer = parseLiteralNode(); break; diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index 2d07c2998e0..bfc9591baa7 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -27,6 +27,7 @@ namespace ts { reScanSlashToken(): SyntaxKind; reScanTemplateToken(): SyntaxKind; scanJsxIdentifier(): SyntaxKind; + scanJsxAttributeValue(): SyntaxKind; reScanJsxToken(): SyntaxKind; scanJsxToken(): SyntaxKind; scanJSDocToken(): SyntaxKind; @@ -817,6 +818,7 @@ namespace ts { reScanSlashToken, reScanTemplateToken, scanJsxIdentifier, + scanJsxAttributeValue, reScanJsxToken, scanJsxToken, scanJSDocToken, @@ -911,7 +913,7 @@ namespace ts { return value; } - function scanString(): string { + function scanString(allowEscapes = true): string { const quote = text.charCodeAt(pos); pos++; let result = ""; @@ -929,7 +931,7 @@ namespace ts { pos++; break; } - if (ch === CharacterCodes.backslash) { + if (ch === CharacterCodes.backslash && allowEscapes) { result += text.substring(start, pos); result += scanEscapeSequence(); start = pos; @@ -1737,6 +1739,20 @@ namespace ts { return token; } + function scanJsxAttributeValue(): SyntaxKind { + startPos = pos; + + switch (text.charCodeAt(pos)) { + case CharacterCodes.doubleQuote: + case CharacterCodes.singleQuote: + tokenValue = scanString(/*allowEscapes*/ false); + return token = SyntaxKind.StringLiteral; + default: + // If this scans anything other than `{`, it's a parse error. + return scan(); + } + } + function scanJSDocToken(): SyntaxKind { if (pos >= end) { return token = SyntaxKind.EndOfFileToken; diff --git a/src/compiler/transformers/jsx.ts b/src/compiler/transformers/jsx.ts index 4bc523a262f..9e6aa507cce 100644 --- a/src/compiler/transformers/jsx.ts +++ b/src/compiler/transformers/jsx.ts @@ -140,7 +140,8 @@ namespace ts { return createLiteral(true); } else if (node.kind === SyntaxKind.StringLiteral) { - return node; + const decoded = tryDecodeEntities((node).text); + return decoded ? createLiteral(decoded, /*location*/ node) : node; } else if (node.kind === SyntaxKind.JsxExpression) { return visitJsxExpression(node); @@ -210,19 +211,31 @@ namespace ts { } /** - * Decodes JSX entities. + * Replace entities like " ", "{", and "�" with the characters they encode. + * See https://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references */ - function decodeEntities(text: string) { - return text.replace(/&(\w+);/g, function(s: any, m: string) { - if (entities[m] !== undefined) { - return String.fromCharCode(entities[m]); + function decodeEntities(text: string): string { + return text.replace(/&((#((\d+)|x([\da-fA-F]+)))|(\w+));/g, (match, _all, _number, _digits, decimal, hex, word) => { + if (decimal) { + return String.fromCharCode(parseInt(decimal, 10)); + } + else if (hex) { + return String.fromCharCode(parseInt(hex, 16)); } else { - return s; + const ch = entities[word]; + // If this is not a valid entity, then just use `match` (replace it with itself, i.e. don't replace) + return ch ? String.fromCharCode(ch) : match; } }); } + /** Like `decodeEntities` but returns `undefined` if there were no entities to decode. */ + function tryDecodeEntities(text: string): string | undefined { + const decoded = decodeEntities(text); + return decoded === text ? undefined : decoded; + } + function getTagName(node: JsxElement | JsxOpeningLikeElement): Expression { if (node.kind === SyntaxKind.JsxElement) { return getTagName((node).openingElement); diff --git a/tests/baselines/reference/tsxReactEmitEntities.js b/tests/baselines/reference/tsxReactEmitEntities.js index 2a5c1ec4963..41c294b8bab 100644 --- a/tests/baselines/reference/tsxReactEmitEntities.js +++ b/tests/baselines/reference/tsxReactEmitEntities.js @@ -9,8 +9,27 @@ declare var React: any;
Dot goes here: · ¬AnEntity;
;
Be careful of "-ed strings!
; +
{{braces}}
; +// Escapes do nothing +
\n
; + +// Also works in string literal attributes +
; +// Does not happen for a string literal that happens to be inside an attribute (and escapes then work) +
; +// Preserves single quotes +
//// [file.js] React.createElement("div", null, "Dot goes here: \u00B7 ¬AnEntity; "); React.createElement("div", null, "Be careful of \"-ed strings!"); +React.createElement("div", null, "{{braces}}"); +// Escapes do nothing +React.createElement("div", null, "\\n"); +// Also works in string literal attributes +React.createElement("div", { attr: "{\u2026}\\" }); +// Does not happen for a string literal that happens to be inside an attribute (and escapes then work) +React.createElement("div", { attr: "{…}\"" }); +// Preserves single quotes +React.createElement("div", { attr: '"' }); diff --git a/tests/baselines/reference/tsxReactEmitEntities.symbols b/tests/baselines/reference/tsxReactEmitEntities.symbols index 5a0274029e4..470c6177842 100644 --- a/tests/baselines/reference/tsxReactEmitEntities.symbols +++ b/tests/baselines/reference/tsxReactEmitEntities.symbols @@ -23,3 +23,30 @@ declare var React: any; >div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22)) >div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22)) +
{{braces}}
; +>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22)) +>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22)) + +// Escapes do nothing +
\n
; +>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22)) +>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22)) + +// Also works in string literal attributes +
; +>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22)) +>attr : Symbol(unknown) +>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22)) + +// Does not happen for a string literal that happens to be inside an attribute (and escapes then work) +
; +>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22)) +>attr : Symbol(unknown) +>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22)) + +// Preserves single quotes +
+>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22)) +>attr : Symbol(unknown) +>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22)) + diff --git a/tests/baselines/reference/tsxReactEmitEntities.types b/tests/baselines/reference/tsxReactEmitEntities.types index 111653ea140..3127e97afc3 100644 --- a/tests/baselines/reference/tsxReactEmitEntities.types +++ b/tests/baselines/reference/tsxReactEmitEntities.types @@ -25,3 +25,36 @@ declare var React: any; >div : any >div : any +
{{braces}}
; +>
{{braces}}
: JSX.Element +>div : any +>div : any + +// Escapes do nothing +
\n
; +>
\n
: JSX.Element +>div : any +>div : any + +// Also works in string literal attributes +
; +>
: JSX.Element +>div : any +>attr : any +>div : any + +// Does not happen for a string literal that happens to be inside an attribute (and escapes then work) +
; +>
: JSX.Element +>div : any +>attr : any +>"{…}\"" : string +>div : any + +// Preserves single quotes +
+>
: JSX.Element +>div : any +>attr : any +>div : any + diff --git a/tests/cases/conformance/jsx/tsxReactEmitEntities.tsx b/tests/cases/conformance/jsx/tsxReactEmitEntities.tsx index 4726008d6be..e4b83ebb34b 100644 --- a/tests/cases/conformance/jsx/tsxReactEmitEntities.tsx +++ b/tests/cases/conformance/jsx/tsxReactEmitEntities.tsx @@ -10,3 +10,13 @@ declare var React: any;
Dot goes here: · ¬AnEntity;
;
Be careful of "-ed strings!
; +
{{braces}}
; +// Escapes do nothing +
\n
; + +// Also works in string literal attributes +
; +// Does not happen for a string literal that happens to be inside an attribute (and escapes then work) +
; +// Preserves single quotes +