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)); }