diff --git a/packages/jsii-rosetta/lib/languages/csharp.ts b/packages/jsii-rosetta/lib/languages/csharp.ts index 112eeed928..88e1bf8baf 100644 --- a/packages/jsii-rosetta/lib/languages/csharp.ts +++ b/packages/jsii-rosetta/lib/languages/csharp.ts @@ -109,7 +109,7 @@ export class CSharpVisitor extends DefaultVisitor { } public identifier( - node: ts.Identifier | ts.StringLiteral, + node: ts.Identifier | ts.StringLiteral | ts.NoSubstitutionTemplateLiteral, renderer: CSharpRenderer, ) { let text = node.text; @@ -339,12 +339,16 @@ export class CSharpVisitor extends DefaultVisitor { } public stringLiteral( - node: ts.StringLiteral, + node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral, renderer: CSharpRenderer, ): OTree { if (renderer.currentContext.stringAsIdentifier) { return this.identifier(node, renderer); } + if (node.text.includes('\n')) { + // Multi-line string literals (@"string") in C# do not do escaping. Only " needs to be doubled. + return new OTree(['@"', node.text.replace(/"/g, '""'), '"']); + } return new OTree([JSON.stringify(node.text)]); } @@ -740,19 +744,32 @@ export class CSharpVisitor extends DefaultVisitor { node: ts.TemplateExpression, context: CSharpRenderer, ): OTree { - const parts = ['$"']; + // If this is a multi-line string literal, we need not quote much, as @"string" literals in C# + // do not perform any quoting. The literal quotes in the text however must be doubled. + const isMultiLine = + !!node.head.rawText?.includes('\n') || + node.templateSpans.some((span) => span.literal.rawText?.includes('\n')); + + const parts = new Array(); if (node.head.rawText) { - parts.push(quoteStringLiteral(node.head.rawText)); + parts.push( + isMultiLine + ? node.head.rawText.replace(/"/g, '""') + : quoteStringLiteral(node.head.rawText), + ); } for (const span of node.templateSpans) { parts.push(`{${context.textOf(span.expression)}}`); if (span.literal.rawText) { - parts.push(quoteStringLiteral(span.literal.rawText)); + parts.push( + isMultiLine + ? span.literal.rawText.replace(/"/g, '""') + : quoteStringLiteral(span.literal.rawText), + ); } } - parts.push('"'); - return new OTree([parts.join('')]); + return new OTree([isMultiLine ? '$@"' : '$"', ...parts, '"']); } protected argumentList( diff --git a/packages/jsii-rosetta/lib/languages/default.ts b/packages/jsii-rosetta/lib/languages/default.ts index 76c0a2bb58..802aa63e8c 100644 --- a/packages/jsii-rosetta/lib/languages/default.ts +++ b/packages/jsii-rosetta/lib/languages/default.ts @@ -46,15 +46,8 @@ export abstract class DefaultVisitor implements AstHandler { } public stringLiteral( - node: ts.StringLiteral, - _children: AstRenderer, - ): OTree { - return new OTree([JSON.stringify(node.text)]); - } - - public noSubstitutionTemplateLiteral( - node: ts.NoSubstitutionTemplateLiteral, - _context: AstRenderer, + node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral, + _renderer: AstRenderer, ): OTree { return new OTree([JSON.stringify(node.text)]); } diff --git a/packages/jsii-rosetta/lib/languages/java.ts b/packages/jsii-rosetta/lib/languages/java.ts index 0b24c501fe..ea0bef5328 100644 --- a/packages/jsii-rosetta/lib/languages/java.ts +++ b/packages/jsii-rosetta/lib/languages/java.ts @@ -476,12 +476,14 @@ export class JavaVisitor extends DefaultVisitor { } if (parameters.length === 0) { - return new OTree([`"${quoteStringLiteral(template)}"`]); + return new OTree([JSON.stringify(quoteStringLiteral(template))]); } return new OTree([ 'String.format(', - `"${quoteStringLiteral(template)}"`, + `"${quoteStringLiteral(template) + // Java does not have multiline string literals, so we must replace literal newlines with %n + .replace(/\n/g, '%n')}"`, ...parameters.reduce( (list, element) => list.concat(', ', element), new Array(), @@ -677,14 +679,19 @@ export class JavaVisitor extends DefaultVisitor { return new OTree(parts); } - public stringLiteral(node: ts.StringLiteral, renderer: JavaRenderer): OTree { - return renderer.currentContext.stringLiteralAsIdentifier - ? this.identifier(node, renderer) - : super.stringLiteral(node, renderer); + public stringLiteral( + node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral, + renderer: JavaRenderer, + ): OTree { + if (renderer.currentContext.stringLiteralAsIdentifier) { + return this.identifier(node, renderer); + } + // Java does not have multiline string literals, so we must replace literal newlines with \n + return new OTree([JSON.stringify(node.text).replace(/\n/g, '\\n')]); } public identifier( - node: ts.Identifier | ts.StringLiteral, + node: ts.Identifier | ts.StringLiteral | ts.NoSubstitutionTemplateLiteral, renderer: JavaRenderer, ): OTree { const nodeText = node.text; diff --git a/packages/jsii-rosetta/lib/languages/python.ts b/packages/jsii-rosetta/lib/languages/python.ts index 208eb8b93f..f058af97e5 100644 --- a/packages/jsii-rosetta/lib/languages/python.ts +++ b/packages/jsii-rosetta/lib/languages/python.ts @@ -700,11 +700,30 @@ export class PythonVisitor extends DefaultVisitor { return context.convert(node.expression); } + public stringLiteral( + node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral, + _context: PythonVisitorContext, + ): OTree { + const rawText = node.text; + if (rawText.includes('\n')) { + return new OTree([ + '"""', + rawText + // Escape all occurrences of back-slash once more + .replace(/\\/g, '\\\\') + // Escape only the first one in triple-quotes + .replace(/"""/g, '\\"""'), + '"""', + ]); + } + return new OTree([JSON.stringify(rawText)]); + } + public templateExpression( node: ts.TemplateExpression, context: PythonVisitorContext, ): OTree { - const parts = ['f"']; + const parts = new Array(); if (node.head.rawText) { parts.push(quoteStringLiteral(node.head.rawText)); } @@ -714,9 +733,10 @@ export class PythonVisitor extends DefaultVisitor { parts.push(quoteStringLiteral(span.literal.rawText)); } } - parts.push('"'); - return new OTree([parts.join('')]); + const quote = parts.some((part) => part.includes('\n')) ? '"""' : '"'; + + return new OTree([`f${quote}`, ...parts, quote]); } public maskingVoidExpression( diff --git a/packages/jsii-rosetta/lib/languages/visualize.ts b/packages/jsii-rosetta/lib/languages/visualize.ts index 7648f4da4a..027b3b17ce 100644 --- a/packages/jsii-rosetta/lib/languages/visualize.ts +++ b/packages/jsii-rosetta/lib/languages/visualize.ts @@ -41,7 +41,7 @@ export class VisualizeAstVisitor implements AstHandler { } public stringLiteral( - node: ts.StringLiteral, + node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral, children: AstRenderer, ): OTree { return this.defaultNode('stringLiteral', node, children); @@ -290,13 +290,6 @@ export class VisualizeAstVisitor implements AstHandler { return this.defaultNode('maskingVoidExpression', node, context); } - public noSubstitutionTemplateLiteral( - node: ts.NoSubstitutionTemplateLiteral, - context: AstRenderer, - ): OTree { - return this.defaultNode('noSubstitutionTemplateLiteral', node, context); - } - private defaultNode( handlerName: string, node: ts.Node, diff --git a/packages/jsii-rosetta/lib/renderer.ts b/packages/jsii-rosetta/lib/renderer.ts index cb68bef1bf..5b432f8a6b 100644 --- a/packages/jsii-rosetta/lib/renderer.ts +++ b/packages/jsii-rosetta/lib/renderer.ts @@ -262,7 +262,7 @@ export class AstRenderer { this, ); } - if (ts.isStringLiteral(tree)) { + if (ts.isStringLiteral(tree) || ts.isNoSubstitutionTemplateLiteral(tree)) { return visitor.stringLiteral(tree, this); } if (ts.isFunctionDeclaration(tree)) { @@ -295,9 +295,6 @@ export class AstRenderer { if (ts.isExpressionStatement(tree)) { return visitor.expressionStatement(tree, this); } - if (ts.isNoSubstitutionTemplateLiteral(tree)) { - return visitor.noSubstitutionTemplateLiteral(tree, this); - } if (ts.isToken(tree)) { return visitor.token(tree, this); } @@ -480,7 +477,10 @@ export interface AstHandler { sourceFile(node: ts.SourceFile, context: AstRenderer): OTree; commentRange(node: CommentSyntax, context: AstRenderer): OTree; importStatement(node: ImportStatement, context: AstRenderer): OTree; - stringLiteral(node: ts.StringLiteral, children: AstRenderer): OTree; + stringLiteral( + node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral, + children: AstRenderer, + ): OTree; functionDeclaration( node: ts.FunctionDeclaration, children: AstRenderer, @@ -572,10 +572,6 @@ export interface AstHandler { node: ts.VoidExpression, context: AstRenderer, ): OTree; - noSubstitutionTemplateLiteral( - node: ts.NoSubstitutionTemplateLiteral, - context: AstRenderer, - ): OTree; // Not a node, called when we recognize a spread element/assignment that is only // '...' and nothing else. diff --git a/packages/jsii-rosetta/test/translations/expressions/string_interpolation.cs b/packages/jsii-rosetta/test/translations/expressions/string_interpolation.cs index 4d5e78ac30..0a9840046f 100644 --- a/packages/jsii-rosetta/test/translations/expressions/string_interpolation.cs +++ b/packages/jsii-rosetta/test/translations/expressions/string_interpolation.cs @@ -1,3 +1,10 @@ string x = "world"; string y = "well"; -Console.WriteLine($"Hello, {x}, it works {y}!"); \ No newline at end of file +Console.WriteLine($"Hello, {x}, it works {y}!"); + +// And now a multi-line expression +Console.WriteLine($@" +Hello, {x}. + +It works {y}! +"); diff --git a/packages/jsii-rosetta/test/translations/expressions/string_interpolation.java b/packages/jsii-rosetta/test/translations/expressions/string_interpolation.java index 7f768fc4f3..5b44bc5aec 100644 --- a/packages/jsii-rosetta/test/translations/expressions/string_interpolation.java +++ b/packages/jsii-rosetta/test/translations/expressions/string_interpolation.java @@ -1,3 +1,6 @@ String x = "world"; String y = "well"; System.out.println(String.format("Hello, %s, it works %s!", x, y)); + +// And now a multi-line expression +System.out.println(String.format("%nHello, %s.%n%nIt works %s!%n", x, y)); diff --git a/packages/jsii-rosetta/test/translations/expressions/string_interpolation.py b/packages/jsii-rosetta/test/translations/expressions/string_interpolation.py index 82840ad68d..c03cb7263f 100644 --- a/packages/jsii-rosetta/test/translations/expressions/string_interpolation.py +++ b/packages/jsii-rosetta/test/translations/expressions/string_interpolation.py @@ -1,3 +1,10 @@ x = "world" y = "well" -print(f"Hello, {x}, it works {y}!") \ No newline at end of file +print(f"Hello, {x}, it works {y}!") + +# And now a multi-line expression +print(f""" +Hello, {x}. + +It works {y}! +""") diff --git a/packages/jsii-rosetta/test/translations/expressions/string_interpolation.ts b/packages/jsii-rosetta/test/translations/expressions/string_interpolation.ts index 0954225c8d..e2e5ae239f 100644 --- a/packages/jsii-rosetta/test/translations/expressions/string_interpolation.ts +++ b/packages/jsii-rosetta/test/translations/expressions/string_interpolation.ts @@ -1,3 +1,10 @@ -const x = "world"; -const y = "well"; -console.log(`Hello, ${x}, it works ${y}!`); \ No newline at end of file +const x = 'world'; +const y = 'well'; +console.log(`Hello, ${x}, it works ${y}!`); + +// And now a multi-line expression +console.log(` +Hello, ${x}. + +It works ${y}! +`); diff --git a/packages/jsii-rosetta/test/translations/expressions/string_literal.cs b/packages/jsii-rosetta/test/translations/expressions/string_literal.cs new file mode 100644 index 0000000000..64c5ebc69d --- /dev/null +++ b/packages/jsii-rosetta/test/translations/expressions/string_literal.cs @@ -0,0 +1,9 @@ +string literal = @" +This si a multiline string literal. + +""It's cool!"". + +YEAH BABY!! + +Litteral \n right here (not a newline!) +"; diff --git a/packages/jsii-rosetta/test/translations/expressions/string_literal.java b/packages/jsii-rosetta/test/translations/expressions/string_literal.java new file mode 100644 index 0000000000..d81e09e1e9 --- /dev/null +++ b/packages/jsii-rosetta/test/translations/expressions/string_literal.java @@ -0,0 +1 @@ +String literal = "\nThis si a multiline string literal.\n\n\"It's cool!\".\n\nYEAH BABY!!\n\nLitteral \\n right here (not a newline!)\n"; diff --git a/packages/jsii-rosetta/test/translations/expressions/string_literal.py b/packages/jsii-rosetta/test/translations/expressions/string_literal.py new file mode 100644 index 0000000000..b9f43b8966 --- /dev/null +++ b/packages/jsii-rosetta/test/translations/expressions/string_literal.py @@ -0,0 +1,9 @@ +literal = """ +This si a multiline string literal. + +"It's cool!". + +YEAH BABY!! + +Litteral \\n right here (not a newline!) +""" diff --git a/packages/jsii-rosetta/test/translations/expressions/string_literal.ts b/packages/jsii-rosetta/test/translations/expressions/string_literal.ts new file mode 100644 index 0000000000..ebff58a601 --- /dev/null +++ b/packages/jsii-rosetta/test/translations/expressions/string_literal.ts @@ -0,0 +1,9 @@ +const literal = ` +This si a multiline string literal. + +"It's cool!". + +YEAH BABY!! + +Litteral \\n right here (not a newline!) +`;