Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 18 additions & 13 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1152,6 +1152,10 @@ namespace ts {
return pipelineEmit(EmitHint.Expression, node);
}

function emitJsxAttributeValue(node: StringLiteral | JsxExpression): Node {
return pipelineEmit(isStringLiteral(node) ? EmitHint.JsxAttributeValue : EmitHint.Unspecified, node);
}

function pipelineEmit(emitHint: EmitHint, node: Node) {
const savedLastNode = lastNode;
const savedLastSubstitution = lastSubstitution;
Expand Down Expand Up @@ -1219,6 +1223,7 @@ namespace ts {
Debug.assert(lastNode === node || lastSubstitution === node);
if (hint === EmitHint.SourceFile) return emitSourceFile(cast(node, isSourceFile));
if (hint === EmitHint.IdentifierName) return emitIdentifier(cast(node, isIdentifier));
if (hint === EmitHint.JsxAttributeValue) return emitLiteral(cast(node, isStringLiteral), /*jsxAttributeEscape*/ true);
if (hint === EmitHint.MappedTypeParameter) return emitMappedTypeParameter(cast(node, isTypeParameterDeclaration));
if (hint === EmitHint.EmbeddedStatement) {
Debug.assertNode(node, isEmptyStatement);
Expand All @@ -1232,7 +1237,7 @@ namespace ts {
case SyntaxKind.TemplateHead:
case SyntaxKind.TemplateMiddle:
case SyntaxKind.TemplateTail:
return emitLiteral(<LiteralExpression>node);
return emitLiteral(<LiteralExpression>node, /*jsxAttributeEscape*/ false);

case SyntaxKind.UnparsedSource:
case SyntaxKind.UnparsedPrepend:
Expand Down Expand Up @@ -1545,7 +1550,7 @@ namespace ts {
case SyntaxKind.StringLiteral:
case SyntaxKind.RegularExpressionLiteral:
case SyntaxKind.NoSubstitutionTemplateLiteral:
return emitLiteral(<LiteralExpression>node);
return emitLiteral(<LiteralExpression>node, /*jsxAttributeEscape*/ false);

// Identifiers
case SyntaxKind.Identifier:
Expand Down Expand Up @@ -1736,7 +1741,7 @@ namespace ts {
// SyntaxKind.NumericLiteral
// SyntaxKind.BigIntLiteral
function emitNumericOrBigIntLiteral(node: NumericLiteral | BigIntLiteral) {
emitLiteral(node);
emitLiteral(node, /*jsxAttributeEscape*/ false);
}

// SyntaxKind.StringLiteral
Expand All @@ -1745,8 +1750,8 @@ namespace ts {
// SyntaxKind.TemplateHead
// SyntaxKind.TemplateMiddle
// SyntaxKind.TemplateTail
function emitLiteral(node: LiteralLikeNode) {
const text = getLiteralTextOfNode(node, printerOptions.neverAsciiEscape);
function emitLiteral(node: LiteralLikeNode, jsxAttributeEscape: boolean) {
const text = getLiteralTextOfNode(node, printerOptions.neverAsciiEscape, jsxAttributeEscape);
if ((printerOptions.sourceMap || printerOptions.inlineSourceMap)
&& (node.kind === SyntaxKind.StringLiteral || isTemplateLiteralKind(node.kind))) {
writeLiteral(text);
Expand Down Expand Up @@ -2279,7 +2284,7 @@ namespace ts {
expression = skipPartiallyEmittedExpressions(expression);
if (isNumericLiteral(expression)) {
// check if numeric literal is a decimal literal that was originally written with a dot
const text = getLiteralTextOfNode(<LiteralExpression>expression, /*neverAsciiEscape*/ true);
const text = getLiteralTextOfNode(<LiteralExpression>expression, /*neverAsciiEscape*/ true, /*jsxAttributeEscape*/ false);
// If he number will be printed verbatim and it doesn't already contain a dot, add one
// if the expression doesn't have any comments that will be emitted.
return !expression.numericLiteralFlags && !stringContains(text, tokenToString(SyntaxKind.DotToken)!);
Expand Down Expand Up @@ -3198,7 +3203,7 @@ namespace ts {

function emitJsxAttribute(node: JsxAttribute) {
emit(node.name);
emitNodeWithPrefix("=", writePunctuation, node.initializer!, emit); // TODO: GH#18217
emitNodeWithPrefix("=", writePunctuation, node.initializer, emitJsxAttributeValue);
}

function emitJsxSpreadAttribute(node: JsxSpreadAttribute) {
Expand Down Expand Up @@ -3731,7 +3736,7 @@ namespace ts {
}
}

function emitNodeWithPrefix(prefix: string, prefixWriter: (s: string) => void, node: Node, emit: (node: Node) => void) {
function emitNodeWithPrefix<T extends Node>(prefix: string, prefixWriter: (s: string) => void, node: T | undefined, emit: (node: T) => void) {
if (node) {
prefixWriter(prefix);
emit(node);
Expand Down Expand Up @@ -4288,20 +4293,20 @@ namespace ts {
return getSourceTextOfNodeFromSourceFile(currentSourceFile!, node, includeTrivia);
}

function getLiteralTextOfNode(node: LiteralLikeNode, neverAsciiEscape: boolean | undefined): string {
function getLiteralTextOfNode(node: LiteralLikeNode, neverAsciiEscape: boolean | undefined, jsxAttributeEscape: boolean): string {
if (node.kind === SyntaxKind.StringLiteral && (<StringLiteral>node).textSourceNode) {
const textSourceNode = (<StringLiteral>node).textSourceNode!;
if (isIdentifier(textSourceNode)) {
return neverAsciiEscape || (getEmitFlags(node) & EmitFlags.NoAsciiEscaping) ?
`"${escapeString(getTextOfNode(textSourceNode))}"` :
return jsxAttributeEscape ? `"${escapeJsxAttributeString(getTextOfNode(textSourceNode))}"` :
neverAsciiEscape || (getEmitFlags(node) & EmitFlags.NoAsciiEscaping) ? `"${escapeString(getTextOfNode(textSourceNode))}"` :
`"${escapeNonAsciiString(getTextOfNode(textSourceNode))}"`;
}
else {
return getLiteralTextOfNode(textSourceNode, neverAsciiEscape);
return getLiteralTextOfNode(textSourceNode, neverAsciiEscape, jsxAttributeEscape);
}
}

return getLiteralText(node, currentSourceFile!, neverAsciiEscape);
return getLiteralText(node, currentSourceFile!, neverAsciiEscape, jsxAttributeEscape);
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5759,6 +5759,7 @@ namespace ts {
MappedTypeParameter, // Emitting a TypeParameterDeclaration inside of a MappedTypeNode
Unspecified, // Emitting an otherwise unspecified node
EmbeddedStatement, // Emitting an embedded statement
JsxAttributeValue, // Emitting a JSX attribute value
}

/* @internal */
Expand Down
108 changes: 72 additions & 36 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ namespace ts {
return emitNode && emitNode.flags || 0;
}

export function getLiteralText(node: LiteralLikeNode, sourceFile: SourceFile, neverAsciiEscape: boolean | undefined) {
export function getLiteralText(node: LiteralLikeNode, sourceFile: SourceFile, neverAsciiEscape: boolean | undefined, jsxAttributeEscape: boolean) {
// If we don't need to downlevel and we can reach the original source text using
// the node's parent reference, then simply get the text as it was originally written.
if (!nodeIsSynthesized(node) && node.parent && !(
Expand All @@ -557,24 +557,29 @@ namespace ts {
return getSourceTextOfNodeFromSourceFile(sourceFile, node);
}

// If a NoSubstitutionTemplateLiteral appears to have a substitution in it, the original text
// had to include a backslash: `not \${a} substitution`.
const escapeText = neverAsciiEscape || (getEmitFlags(node) & EmitFlags.NoAsciiEscaping) ? escapeString : escapeNonAsciiString;

// If we can't reach the original source text, use the canonical form if it's a number,
// or a (possibly escaped) quoted form of the original text if it's string-like.
switch (node.kind) {
case SyntaxKind.StringLiteral:
case SyntaxKind.StringLiteral: {
const escapeText = jsxAttributeEscape ? escapeJsxAttributeString :
neverAsciiEscape || (getEmitFlags(node) & EmitFlags.NoAsciiEscaping) ? escapeString :
escapeNonAsciiString;
if ((<StringLiteral>node).singleQuote) {
return "'" + escapeText(node.text, CharacterCodes.singleQuote) + "'";
}
else {
return '"' + escapeText(node.text, CharacterCodes.doubleQuote) + '"';
}
}
case SyntaxKind.NoSubstitutionTemplateLiteral:
case SyntaxKind.TemplateHead:
case SyntaxKind.TemplateMiddle:
case SyntaxKind.TemplateTail:
case SyntaxKind.TemplateTail: {
// If a NoSubstitutionTemplateLiteral appears to have a substitution in it, the original text
// had to include a backslash: `not \${a} substitution`.
const escapeText = neverAsciiEscape || (getEmitFlags(node) & EmitFlags.NoAsciiEscaping) ? escapeString :
escapeNonAsciiString;

const rawText = (<TemplateLiteralLikeNode>node).rawText || escapeTemplateSubstitution(escapeText(node.text, CharacterCodes.backtick));
switch (node.kind) {
case SyntaxKind.NoSubstitutionTemplateLiteral:
Expand All @@ -587,6 +592,7 @@ namespace ts {
return "}" + rawText + "`";
}
break;
}
case SyntaxKind.NumericLiteral:
case SyntaxKind.BigIntLiteral:
case SyntaxKind.RegularExpressionLiteral:
Expand Down Expand Up @@ -3312,6 +3318,25 @@ namespace ts {
"\u0085": "\\u0085" // nextLine
});

function encodeUtf16EscapeSequence(charCode: number): string {
const hexCharCode = charCode.toString(16).toUpperCase();
const paddedHexCode = ("0000" + hexCharCode).slice(-4);
return "\\u" + paddedHexCode;
}

function getReplacement(c: string, offset: number, input: string) {
if (c.charCodeAt(0) === CharacterCodes.nullCharacter) {
const lookAhead = input.charCodeAt(offset + c.length);
if (lookAhead >= CharacterCodes._0 && lookAhead <= CharacterCodes._9) {
// If the null character is followed by digits, print as a hex escape to prevent the result from parsing as an octal (which is forbidden in strict mode)
return "\\x00";
}
// Otherwise, keep printing a literal \0 for the null character
return "\\0";
}
return escapedCharsMap.get(c) || encodeUtf16EscapeSequence(c.charCodeAt(0));
}

/**
* Based heavily on the abstract 'Quote'/'QuoteJSONString' operation from ECMA-262 (24.3.2.2),
* but augmented for a few select characters (e.g. lineSeparator, paragraphSeparator, nextLine)
Expand All @@ -3325,6 +3350,46 @@ namespace ts {
return s.replace(escapedCharsRegExp, getReplacement);
}

const nonAsciiCharacters = /[^\u0000-\u007F]/g;
export function escapeNonAsciiString(s: string, quoteChar?: CharacterCodes.doubleQuote | CharacterCodes.singleQuote | CharacterCodes.backtick): string {
s = escapeString(s, quoteChar);
// Replace non-ASCII characters with '\uNNNN' escapes if any exist.
// Otherwise just return the original string.
return nonAsciiCharacters.test(s) ?
s.replace(nonAsciiCharacters, c => encodeUtf16EscapeSequence(c.charCodeAt(0))) :
s;
}

// This consists of the first 19 unprintable ASCII characters, JSX canonical escapes, lineSeparator,
// paragraphSeparator, and nextLine. The latter three are just desirable to suppress new lines in
// the language service. These characters should be escaped when printing, and if any characters are added,
// the map below must be updated.
const jsxDoubleQuoteEscapedCharsRegExp = /[\"\u0000-\u001f\u2028\u2029\u0085]/g;
const jsxSingleQuoteEscapedCharsRegExp = /[\'\u0000-\u001f\u2028\u2029\u0085]/g;
const jsxEscapedCharsMap = createMapFromTemplate({
"\"": "&quot;",
"\'": "&apos;"
});

function encodeJsxCharacterEntity(charCode: number): string {
const hexCharCode = charCode.toString(16).toUpperCase();
return "&#x" + hexCharCode + ";";
}

function getJsxAttributeStringReplacement(c: string) {
if (c.charCodeAt(0) === CharacterCodes.nullCharacter) {
return "&#0;";
}
return jsxEscapedCharsMap.get(c) || encodeJsxCharacterEntity(c.charCodeAt(0));
}

export function escapeJsxAttributeString(s: string, quoteChar?: CharacterCodes.doubleQuote | CharacterCodes.singleQuote) {
const escapedCharsRegExp =
quoteChar === CharacterCodes.singleQuote ? jsxSingleQuoteEscapedCharsRegExp :
jsxDoubleQuoteEscapedCharsRegExp;
return s.replace(escapedCharsRegExp, getJsxAttributeStringReplacement);
}

/**
* Strip off existed surrounding single quotes, double quotes, or backticks from a given string
*
Expand All @@ -3344,40 +3409,11 @@ namespace ts {
charCode === CharacterCodes.backtick;
}

function getReplacement(c: string, offset: number, input: string) {
if (c.charCodeAt(0) === CharacterCodes.nullCharacter) {
const lookAhead = input.charCodeAt(offset + c.length);
if (lookAhead >= CharacterCodes._0 && lookAhead <= CharacterCodes._9) {
// If the null character is followed by digits, print as a hex escape to prevent the result from parsing as an octal (which is forbidden in strict mode)
return "\\x00";
}
// Otherwise, keep printing a literal \0 for the null character
return "\\0";
}
return escapedCharsMap.get(c) || get16BitUnicodeEscapeSequence(c.charCodeAt(0));
}

export function isIntrinsicJsxName(name: __String | string) {
const ch = (name as string).charCodeAt(0);
return (ch >= CharacterCodes.a && ch <= CharacterCodes.z) || stringContains((name as string), "-");
}

function get16BitUnicodeEscapeSequence(charCode: number): string {
const hexCharCode = charCode.toString(16).toUpperCase();
const paddedHexCode = ("0000" + hexCharCode).slice(-4);
return "\\u" + paddedHexCode;
}

const nonAsciiCharacters = /[^\u0000-\u007F]/g;
export function escapeNonAsciiString(s: string, quoteChar?: CharacterCodes.doubleQuote | CharacterCodes.singleQuote | CharacterCodes.backtick): string {
s = escapeString(s, quoteChar);
// Replace non-ASCII characters with '\uNNNN' escapes if any exist.
// Otherwise just return the original string.
return nonAsciiCharacters.test(s) ?
s.replace(nonAsciiCharacters, c => get16BitUnicodeEscapeSequence(c.charCodeAt(0))) :
s;
}

const indentStrings: string[] = ["", " "];
export function getIndentString(level: number) {
if (indentStrings[level] === undefined) {
Expand Down
Loading