From ef892743ec523a431a10b1232647bc30e8d9f3f9 Mon Sep 17 00:00:00 2001 From: Bjarki Date: Sun, 27 Sep 2020 01:45:38 +0000 Subject: [PATCH] feat(compiler): support tagged template literals in code generator (#39122) Add a TaggedTemplateExpr to represent tagged template literals in Angular's syntax tree (more specifically Expression in output_ast.ts). Also update classes that implement ExpressionVisitor to add support for tagged template literals in different contexts, such as JIT compilation and conversion to JS. Partial support for tagged template literals had already been implemented to support the $localize tag used by Angular's i18n framework. Where applicable, this code was refactored to support arbitrary tags, although completely replacing the i18n-specific support for the $localize tag with the new generic support for tagged template literals may not be completely trivial, and is left as future work. PR Close #39122 --- .../src/rendering/esm5_rendering_formatter.ts | 3 +- .../ngcc/test/rendering/renderer_spec.ts | 2 +- .../src/ngtsc/transform/src/transform.ts | 2 +- .../src/ngtsc/translator/index.ts | 2 +- .../src/ngtsc/translator/src/translator.ts | 33 ++++++-- .../ngtsc/translator/src/type_translator.ts | 4 + .../src/transformers/node_emitter.ts | 6 +- packages/compiler/src/compiler.ts | 2 +- packages/compiler/src/constant_pool.ts | 1 + .../compiler/src/output/abstract_emitter.ts | 11 +++ .../src/output/abstract_js_emitter.ts | 50 ++++++++--- packages/compiler/src/output/output_ast.ts | 84 +++++++++++++++++-- .../compiler/src/output/output_interpreter.ts | 9 ++ 13 files changed, 173 insertions(+), 36 deletions(-) diff --git a/packages/compiler-cli/ngcc/src/rendering/esm5_rendering_formatter.ts b/packages/compiler-cli/ngcc/src/rendering/esm5_rendering_formatter.ts index 8cb3e68310..2f5fdc4b5f 100644 --- a/packages/compiler-cli/ngcc/src/rendering/esm5_rendering_formatter.ts +++ b/packages/compiler-cli/ngcc/src/rendering/esm5_rendering_formatter.ts @@ -65,8 +65,7 @@ export class Esm5RenderingFormatter extends EsmRenderingFormatter { */ printStatement(stmt: Statement, sourceFile: ts.SourceFile, importManager: ImportManager): string { const node = translateStatement( - stmt, importManager, - {downlevelLocalizedStrings: true, downlevelVariableDeclarations: true}); + stmt, importManager, {downlevelTaggedTemplates: true, downlevelVariableDeclarations: true}); const code = this.printer.printNode(ts.EmitHint.Unspecified, node, sourceFile); return code; diff --git a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts index 98e7a04856..95ad53d675 100644 --- a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts @@ -66,7 +66,7 @@ class TestRenderingFormatter implements RenderingFormatter { printStatement(stmt: Statement, sourceFile: ts.SourceFile, importManager: ImportManager): string { const node = translateStatement( stmt, importManager, - {downlevelLocalizedStrings: this.isEs5, downlevelVariableDeclarations: this.isEs5}); + {downlevelTaggedTemplates: this.isEs5, downlevelVariableDeclarations: this.isEs5}); const code = this.printer.printNode(ts.EmitHint.Unspecified, node, sourceFile); return `// TRANSPILED\n${code}`; diff --git a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts index 96ea10d553..76ebcef30f 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts @@ -280,7 +280,7 @@ function transformIvySourceFile( const constants = constantPool.statements.map(stmt => translateStatement(stmt, importManager, { recordWrappedNodeExpr, - downlevelLocalizedStrings: downlevelTranslatedCode, + downlevelTaggedTemplates: downlevelTranslatedCode, downlevelVariableDeclarations: downlevelTranslatedCode, })); diff --git a/packages/compiler-cli/src/ngtsc/translator/index.ts b/packages/compiler-cli/src/ngtsc/translator/index.ts index 8c0dfef49f..be45c20340 100644 --- a/packages/compiler-cli/src/ngtsc/translator/index.ts +++ b/packages/compiler-cli/src/ngtsc/translator/index.ts @@ -12,5 +12,5 @@ export {Context} from './src/context'; export {ImportManager} from './src/import_manager'; export {ExpressionTranslatorVisitor, RecordWrappedNodeExprFn, TranslatorOptions} from './src/translator'; export {translateType} from './src/type_translator'; -export {attachComments, TypeScriptAstFactory} from './src/typescript_ast_factory'; +export {attachComments, createTemplateMiddle, createTemplateTail, TypeScriptAstFactory} from './src/typescript_ast_factory'; export {translateExpression, translateStatement} from './src/typescript_translator'; diff --git a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts index 2a5c16f61a..9f9a37c1e1 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import * as o from '@angular/compiler'; +import {createTaggedTemplate} from 'typescript'; import {AstFactory, BinaryOperator, ObjectLiteralProperty, SourceMapRange, TemplateElement, TemplateLiteral, UnaryOperator} from './api/ast_factory'; import {ImportGenerator} from './api/import_generator'; @@ -38,21 +39,21 @@ const BINARY_OPERATORS = new Map([ export type RecordWrappedNodeExprFn = (expr: TExpression) => void; export interface TranslatorOptions { - downlevelLocalizedStrings?: boolean; + downlevelTaggedTemplates?: boolean; downlevelVariableDeclarations?: boolean; recordWrappedNodeExpr?: RecordWrappedNodeExprFn; } export class ExpressionTranslatorVisitor implements o.ExpressionVisitor, o.StatementVisitor { - private downlevelLocalizedStrings: boolean; + private downlevelTaggedTemplates: boolean; private downlevelVariableDeclarations: boolean; private recordWrappedNodeExpr: RecordWrappedNodeExprFn; constructor( private factory: AstFactory, private imports: ImportGenerator, options: TranslatorOptions) { - this.downlevelLocalizedStrings = options.downlevelLocalizedStrings === true; + this.downlevelTaggedTemplates = options.downlevelTaggedTemplates === true; this.downlevelVariableDeclarations = options.downlevelVariableDeclarations === true; this.recordWrappedNodeExpr = options.recordWrappedNodeExpr || (() => {}); } @@ -168,6 +169,19 @@ export class ExpressionTranslatorVisitor implements o.E ast.sourceSpan); } + visitTaggedTemplateExpr(ast: o.TaggedTemplateExpr, context: Context): TExpression { + return this.setSourceMapRange( + this.createTaggedTemplateExpression(ast.tag.visitExpression(this, context), { + elements: ast.template.elements.map(e => createTemplateElement({ + cooked: e.text, + raw: e.rawText, + range: e.sourceSpan ?? ast.sourceSpan, + })), + expressions: ast.template.expressions.map(e => e.visitExpression(this, context)) + }), + ast.sourceSpan); + } + visitInstantiateExpr(ast: o.InstantiateExpr, context: Context): TExpression { return this.factory.createNewExpression( ast.classExpr.visitExpression(this, context), @@ -202,13 +216,14 @@ export class ExpressionTranslatorVisitor implements o.E } const localizeTag = this.factory.createIdentifier('$localize'); + return this.setSourceMapRange( + this.createTaggedTemplateExpression(localizeTag, {elements, expressions}), ast.sourceSpan); + } - // Now choose which implementation to use to actually create the necessary AST nodes. - const localizeCall = this.downlevelLocalizedStrings ? - this.createES5TaggedTemplateFunctionCall(localizeTag, {elements, expressions}) : - this.factory.createTaggedTemplate(localizeTag, {elements, expressions}); - - return this.setSourceMapRange(localizeCall, ast.sourceSpan); + private createTaggedTemplateExpression(tag: TExpression, template: TemplateLiteral): + TExpression { + return this.downlevelTaggedTemplates ? this.createES5TaggedTemplateFunctionCall(tag, template) : + this.factory.createTaggedTemplate(tag, template); } /** diff --git a/packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts index 180d221f7a..15d2f9c6bd 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts @@ -98,6 +98,10 @@ export class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor throw new Error('Method not implemented.'); } + visitTaggedTemplateExpr(ast: o.TaggedTemplateExpr, context: Context): never { + throw new Error('Method not implemented.'); + } + visitInstantiateExpr(ast: o.InstantiateExpr, context: Context): never { throw new Error('Method not implemented.'); } diff --git a/packages/compiler-cli/src/transformers/node_emitter.ts b/packages/compiler-cli/src/transformers/node_emitter.ts index 6a27f1798b..ae9df7a952 100644 --- a/packages/compiler-cli/src/transformers/node_emitter.ts +++ b/packages/compiler-cli/src/transformers/node_emitter.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LeadingComment, leadingComment, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, LocalizedString, NotExpr, ParseSourceFile, ParseSourceSpan, PartialModule, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StmtModifier, ThrowStmt, TryCatchStmt, TypeofExpr, UnaryOperator, UnaryOperatorExpr, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler'; +import {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LeadingComment, leadingComment, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, LocalizedString, NotExpr, ParseSourceFile, ParseSourceSpan, PartialModule, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StmtModifier, TaggedTemplateExpr, ThrowStmt, TryCatchStmt, TypeofExpr, UnaryOperator, UnaryOperatorExpr, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler'; import * as ts from 'typescript'; import {attachComments} from '../ngtsc/translator'; @@ -544,6 +544,10 @@ export class NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor { expr.args.map(arg => arg.visitExpression(this, null)))); } + visitTaggedTemplateExpr(expr: TaggedTemplateExpr): RecordedNode { + throw new Error('tagged templates are not supported in pre-ivy mode.'); + } + visitInstantiateExpr(expr: InstantiateExpr): RecordedNode { return this.postProcess( expr, diff --git a/packages/compiler/src/compiler.ts b/packages/compiler/src/compiler.ts index a1041f372e..ae5f1aae66 100644 --- a/packages/compiler/src/compiler.ts +++ b/packages/compiler/src/compiler.ts @@ -78,7 +78,7 @@ export * from './ml_parser/tags'; export {LexerRange} from './ml_parser/lexer'; export * from './ml_parser/xml_parser'; export {NgModuleCompiler} from './ng_module_compiler'; -export {ArrayType, AssertNotNull, DYNAMIC_TYPE, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinType, BuiltinTypeName, BuiltinVar, CastExpr, ClassField, ClassMethod, ClassStmt, CommaExpr, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, ExternalReference, literalMap, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, NONE_TYPE, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, ThrowStmt, TryCatchStmt, Type, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement, STRING_TYPE, TypeofExpr, collectExternalReferences, jsDocComment, leadingComment, LeadingComment, JSDocComment, UnaryOperator, UnaryOperatorExpr, LocalizedString} from './output/output_ast'; +export {ArrayType, AssertNotNull, DYNAMIC_TYPE, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinType, BuiltinTypeName, BuiltinVar, CastExpr, ClassField, ClassMethod, ClassStmt, CommaExpr, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, ExternalReference, literalMap, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, NONE_TYPE, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, TaggedTemplateExpr, TemplateLiteral, TemplateLiteralElement, ThrowStmt, TryCatchStmt, Type, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement, STRING_TYPE, TypeofExpr, collectExternalReferences, jsDocComment, leadingComment, LeadingComment, JSDocComment, UnaryOperator, UnaryOperatorExpr, LocalizedString} from './output/output_ast'; export {EmitterVisitorContext} from './output/abstract_emitter'; export {JitEvaluator} from './output/output_jit'; export * from './output/ts_emitter'; diff --git a/packages/compiler/src/constant_pool.ts b/packages/compiler/src/constant_pool.ts index 0046f7cc87..af5f2f2f4f 100644 --- a/packages/compiler/src/constant_pool.ts +++ b/packages/compiler/src/constant_pool.ts @@ -324,6 +324,7 @@ class KeyVisitor implements o.ExpressionVisitor { visitWritePropExpr = invalid; visitInvokeMethodExpr = invalid; visitInvokeFunctionExpr = invalid; + visitTaggedTemplateExpr = invalid; visitInstantiateExpr = invalid; visitConditionalExpr = invalid; visitNotExpr = invalid; diff --git a/packages/compiler/src/output/abstract_emitter.ts b/packages/compiler/src/output/abstract_emitter.ts index bbb561bacc..388cb3e8aa 100644 --- a/packages/compiler/src/output/abstract_emitter.ts +++ b/packages/compiler/src/output/abstract_emitter.ts @@ -344,6 +344,17 @@ export abstract class AbstractEmitterVisitor implements o.StatementVisitor, o.Ex ctx.print(expr, `)`); return null; } + visitTaggedTemplateExpr(expr: o.TaggedTemplateExpr, ctx: EmitterVisitorContext): any { + expr.tag.visitExpression(this, ctx); + ctx.print(expr, '`' + expr.template.elements[0].rawText); + for (let i = 1; i < expr.template.elements.length; i++) { + ctx.print(expr, '${'); + expr.template.expressions[i - 1].visitExpression(this, ctx); + ctx.print(expr, `}${expr.template.elements[i].rawText}`); + } + ctx.print(expr, '`'); + return null; + } visitWrappedNodeExpr(ast: o.WrappedNodeExpr, ctx: EmitterVisitorContext): any { throw new Error('Abstract emitter cannot visit WrappedNodeExpr.'); } diff --git a/packages/compiler/src/output/abstract_js_emitter.ts b/packages/compiler/src/output/abstract_js_emitter.ts index 7afd8159a0..b0054f02db 100644 --- a/packages/compiler/src/output/abstract_js_emitter.ts +++ b/packages/compiler/src/output/abstract_js_emitter.ts @@ -10,6 +10,21 @@ import {AbstractEmitterVisitor, CATCH_ERROR_VAR, CATCH_STACK_VAR, EmitterVisitorContext, escapeIdentifier} from './abstract_emitter'; import * as o from './output_ast'; +/** + * In TypeScript, tagged template functions expect a "template object", which is an array of + * "cooked" strings plus a `raw` property that contains an array of "raw" strings. This is + * typically constructed with a function called `__makeTemplateObject(cooked, raw)`, but it may not + * be available in all environments. + * + * This is a JavaScript polyfill that uses __makeTemplateObject when it's available, but otherwise + * creates an inline helper with the same functionality. + * + * In the inline function, if `Object.defineProperty` is available we use that to attach the `raw` + * array. + */ +const makeTemplateObjectPolyfill = + '(this&&this.__makeTemplateObject||function(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e})'; + export abstract class AbstractJsEmitterVisitor extends AbstractEmitterVisitor { constructor() { super(false); @@ -115,6 +130,27 @@ export abstract class AbstractJsEmitterVisitor extends AbstractEmitterVisitor { } return null; } + visitTaggedTemplateExpr(ast: o.TaggedTemplateExpr, ctx: EmitterVisitorContext): any { + // The following convoluted piece of code is effectively the downlevelled equivalent of + // ``` + // tag`...` + // ``` + // which is effectively like: + // ``` + // tag(__makeTemplateObject(cooked, raw), expression1, expression2, ...); + // ``` + const elements = ast.template.elements; + ast.tag.visitExpression(this, ctx); + ctx.print(ast, `(${makeTemplateObjectPolyfill}(`); + ctx.print(ast, `[${elements.map(part => escapeIdentifier(part.text, false)).join(', ')}], `); + ctx.print(ast, `[${elements.map(part => escapeIdentifier(part.rawText, false)).join(', ')}])`); + ast.template.expressions.forEach(expression => { + ctx.print(ast, ', '); + expression.visitExpression(this, ctx); + }); + ctx.print(ast, ')'); + return null; + } visitFunctionExpr(ast: o.FunctionExpr, ctx: EmitterVisitorContext): any { ctx.print(ast, `function${ast.name ? ' ' + ast.name : ''}(`); this._visitParams(ast.params, ctx); @@ -161,19 +197,7 @@ export abstract class AbstractJsEmitterVisitor extends AbstractEmitterVisitor { // ``` // $localize(__makeTemplateObject(cooked, raw), expression1, expression2, ...); // ``` - // - // The `$localize` function expects a "template object", which is an array of "cooked" strings - // plus a `raw` property that contains an array of "raw" strings. - // - // In some environments a helper function called `__makeTemplateObject(cooked, raw)` might be - // available, in which case we use that. Otherwise we must create our own helper function - // inline. - // - // In the inline function, if `Object.defineProperty` is available we use that to attach the - // `raw` array. - ctx.print( - ast, - '$localize((this&&this.__makeTemplateObject||function(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e})('); + ctx.print(ast, `$localize(${makeTemplateObjectPolyfill}(`); const parts = [ast.serializeI18nHead()]; for (let i = 1; i < ast.messageParts.length; i++) { parts.push(ast.serializeI18nTemplatePart(i)); diff --git a/packages/compiler/src/output/output_ast.ts b/packages/compiler/src/output/output_ast.ts index d74bff4389..bf14072310 100644 --- a/packages/compiler/src/output/output_ast.ts +++ b/packages/compiler/src/output/output_ast.ts @@ -126,20 +126,26 @@ export function nullSafeIsEquivalent( - base: T[], other: T[]) { +function areAllEquivalentPredicate( + base: T[], other: T[], equivalentPredicate: (baseElement: T, otherElement: T) => boolean) { const len = base.length; if (len !== other.length) { return false; } for (let i = 0; i < len; i++) { - if (!base[i].isEquivalent(other[i])) { + if (!equivalentPredicate(base[i], other[i])) { return false; } } return true; } +export function areAllEquivalent( + base: T[], other: T[]) { + return areAllEquivalentPredicate( + base, other, (baseElement: T, otherElement: T) => baseElement.isEquivalent(otherElement)); +} + export abstract class Expression { public type: Type|null; public sourceSpan: ParseSourceSpan|null; @@ -468,6 +474,30 @@ export class InvokeFunctionExpr extends Expression { } +export class TaggedTemplateExpr extends Expression { + constructor( + public tag: Expression, public template: TemplateLiteral, type?: Type|null, + sourceSpan?: ParseSourceSpan|null) { + super(type, sourceSpan); + } + + isEquivalent(e: Expression): boolean { + return e instanceof TaggedTemplateExpr && this.tag.isEquivalent(e.tag) && + areAllEquivalentPredicate( + this.template.elements, e.template.elements, (a, b) => a.text === b.text) && + areAllEquivalent(this.template.expressions, e.template.expressions); + } + + isConstant() { + return false; + } + + visitExpression(visitor: ExpressionVisitor, context: any): any { + return visitor.visitTaggedTemplateExpr(this, context); + } +} + + export class InstantiateExpr extends Expression { constructor( public classExpr: Expression, public args: Expression[], type?: Type|null, @@ -510,6 +540,23 @@ export class LiteralExpr extends Expression { } } +export class TemplateLiteral { + constructor(public elements: TemplateLiteralElement[], public expressions: Expression[]) {} +} +export class TemplateLiteralElement { + rawText: string; + constructor(public text: string, public sourceSpan?: ParseSourceSpan, rawText?: string) { + // If `rawText` is not provided, try to extract the raw string from its + // associated `sourceSpan`. If that is also not available, "fake" the raw + // string instead by escaping the following control sequences: + // - "\" would otherwise indicate that the next character is a control character. + // - "`" and "${" are template string control sequences that would otherwise prematurely + // indicate the end of the template literal element. + this.rawText = + rawText ?? sourceSpan?.toString() ?? escapeForTemplateLiteral(escapeSlashes(text)); + } +} + export abstract class MessagePiece { constructor(public text: string, public sourceSpan: ParseSourceSpan) {} } @@ -603,7 +650,7 @@ export interface CookedRawString { const escapeSlashes = (str: string): string => str.replace(/\\/g, '\\\\'); const escapeStartingColon = (str: string): string => str.replace(/^:/, '\\:'); const escapeColons = (str: string): string => str.replace(/:/g, '\\:'); -const escapeForMessagePart = (str: string): string => +const escapeForTemplateLiteral = (str: string): string => str.replace(/`/g, '\\`').replace(/\${/g, '$\\{'); /** @@ -625,13 +672,13 @@ function createCookedRawString( if (metaBlock === '') { return { cooked: messagePart, - raw: escapeForMessagePart(escapeStartingColon(escapeSlashes(messagePart))), + raw: escapeForTemplateLiteral(escapeStartingColon(escapeSlashes(messagePart))), range, }; } else { return { cooked: `:${metaBlock}:${messagePart}`, - raw: escapeForMessagePart( + raw: escapeForTemplateLiteral( `:${escapeColons(escapeSlashes(metaBlock))}:${escapeSlashes(messagePart)}`), range, }; @@ -953,6 +1000,7 @@ export interface ExpressionVisitor { visitWritePropExpr(expr: WritePropExpr, context: any): any; visitInvokeMethodExpr(ast: InvokeMethodExpr, context: any): any; visitInvokeFunctionExpr(ast: InvokeFunctionExpr, context: any): any; + visitTaggedTemplateExpr(ast: TaggedTemplateExpr, context: any): any; visitInstantiateExpr(ast: InstantiateExpr, context: any): any; visitLiteralExpr(ast: LiteralExpr, context: any): any; visitLocalizedString(ast: LocalizedString, context: any): any; @@ -1275,6 +1323,17 @@ export class AstTransformer implements StatementVisitor, ExpressionVisitor { context); } + visitTaggedTemplateExpr(ast: TaggedTemplateExpr, context: any): any { + return this.transformExpr( + new TaggedTemplateExpr( + ast.tag.visitExpression(this, context), + new TemplateLiteral( + ast.template.elements, + ast.template.expressions.map((e) => e.visitExpression(this, context))), + ast.type, ast.sourceSpan), + context); + } + visitInstantiateExpr(ast: InstantiateExpr, context: any): any { return this.transformExpr( new InstantiateExpr( @@ -1378,7 +1437,7 @@ export class AstTransformer implements StatementVisitor, ExpressionVisitor { return this.transformExpr( new CommaExpr(this.visitAllExpressions(ast.parts, context), ast.sourceSpan), context); } - visitAllExpressions(exprs: Expression[], context: any): Expression[] { + visitAllExpressions(exprs: T[], context: any): T[] { return exprs.map(expr => expr.visitExpression(this, context)); } @@ -1524,6 +1583,11 @@ export class RecursiveAstVisitor implements StatementVisitor, ExpressionVisitor this.visitAllExpressions(ast.args, context); return this.visitExpression(ast, context); } + visitTaggedTemplateExpr(ast: TaggedTemplateExpr, context: any): any { + ast.tag.visitExpression(this, context); + this.visitAllExpressions(ast.template.expressions, context); + return this.visitExpression(ast, context); + } visitInstantiateExpr(ast: InstantiateExpr, context: any): any { ast.classExpr.visitExpression(this, context); this.visitAllExpressions(ast.args, context); @@ -1808,6 +1872,12 @@ export function ifStmt( return new IfStmt(condition, thenClause, elseClause, sourceSpan, leadingComments); } +export function taggedTemplate( + tag: Expression, template: TemplateLiteral, type?: Type|null, + sourceSpan?: ParseSourceSpan|null): TaggedTemplateExpr { + return new TaggedTemplateExpr(tag, template, type, sourceSpan); +} + export function literal( value: any, type?: Type|null, sourceSpan?: ParseSourceSpan|null): LiteralExpr { return new LiteralExpr(value, type, sourceSpan); diff --git a/packages/compiler/src/output/output_interpreter.ts b/packages/compiler/src/output/output_interpreter.ts index 72ae1871ca..22d7dbec83 100644 --- a/packages/compiler/src/output/output_interpreter.ts +++ b/packages/compiler/src/output/output_interpreter.ts @@ -197,6 +197,15 @@ class StatementInterpreter implements o.StatementVisitor, o.ExpressionVisitor { return fn.apply(null, args); } } + visitTaggedTemplateExpr(expr: o.TaggedTemplateExpr, ctx: _ExecutionContext): any { + const templateElements = expr.template.elements.map((e) => e.text); + Object.defineProperty( + templateElements, 'raw', {value: expr.template.elements.map((e) => e.rawText)}); + const args = this.visitAllExpressions(expr.template.expressions, ctx); + args.unshift(templateElements); + const tag = expr.tag.visitExpression(this, ctx); + return tag.apply(null, args); + } visitReturnStmt(stmt: o.ReturnStatement, ctx: _ExecutionContext): any { return new ReturnValue(stmt.value.visitExpression(this, ctx)); }