From 5a49982c9cc62bd26ff1cc1442ba73aca023ad3b 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 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. --- .../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 8cb3e68310e1..2f5fdc4b5f91 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 98e7a0485625..95ad53d67574 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 96ea10d553d1..76ebcef30feb 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 8c0dfef49f72..be45c2034089 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 2a5c16f61a57..9f9a37c1e18e 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 180d221f7a01..15d2f9c6bdb3 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 6a27f1798bf3..ae9df7a952bd 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 a1041f372eff..ae5f1aae668f 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 0046f7cc8790..af5f2f2f4f02 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 bbb561bacc8a..388cb3e8aa34 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 7afd8159a033..b0054f02db80 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 d74bff43891b..bf14072310da 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 72ae1871ca44..22d7dbec8366 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)); }