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
This commit is contained in:
parent
e92d8a8e8f
commit
ef892743ec
@ -65,8 +65,7 @@ export class Esm5RenderingFormatter extends EsmRenderingFormatter {
|
|||||||
*/
|
*/
|
||||||
printStatement(stmt: Statement, sourceFile: ts.SourceFile, importManager: ImportManager): string {
|
printStatement(stmt: Statement, sourceFile: ts.SourceFile, importManager: ImportManager): string {
|
||||||
const node = translateStatement(
|
const node = translateStatement(
|
||||||
stmt, importManager,
|
stmt, importManager, {downlevelTaggedTemplates: true, downlevelVariableDeclarations: true});
|
||||||
{downlevelLocalizedStrings: true, downlevelVariableDeclarations: true});
|
|
||||||
const code = this.printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
|
const code = this.printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
|
||||||
|
|
||||||
return code;
|
return code;
|
||||||
|
@ -66,7 +66,7 @@ class TestRenderingFormatter implements RenderingFormatter {
|
|||||||
printStatement(stmt: Statement, sourceFile: ts.SourceFile, importManager: ImportManager): string {
|
printStatement(stmt: Statement, sourceFile: ts.SourceFile, importManager: ImportManager): string {
|
||||||
const node = translateStatement(
|
const node = translateStatement(
|
||||||
stmt, importManager,
|
stmt, importManager,
|
||||||
{downlevelLocalizedStrings: this.isEs5, downlevelVariableDeclarations: this.isEs5});
|
{downlevelTaggedTemplates: this.isEs5, downlevelVariableDeclarations: this.isEs5});
|
||||||
const code = this.printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
|
const code = this.printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
|
||||||
|
|
||||||
return `// TRANSPILED\n${code}`;
|
return `// TRANSPILED\n${code}`;
|
||||||
|
@ -280,7 +280,7 @@ function transformIvySourceFile(
|
|||||||
const constants =
|
const constants =
|
||||||
constantPool.statements.map(stmt => translateStatement(stmt, importManager, {
|
constantPool.statements.map(stmt => translateStatement(stmt, importManager, {
|
||||||
recordWrappedNodeExpr,
|
recordWrappedNodeExpr,
|
||||||
downlevelLocalizedStrings: downlevelTranslatedCode,
|
downlevelTaggedTemplates: downlevelTranslatedCode,
|
||||||
downlevelVariableDeclarations: downlevelTranslatedCode,
|
downlevelVariableDeclarations: downlevelTranslatedCode,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -12,5 +12,5 @@ export {Context} from './src/context';
|
|||||||
export {ImportManager} from './src/import_manager';
|
export {ImportManager} from './src/import_manager';
|
||||||
export {ExpressionTranslatorVisitor, RecordWrappedNodeExprFn, TranslatorOptions} from './src/translator';
|
export {ExpressionTranslatorVisitor, RecordWrappedNodeExprFn, TranslatorOptions} from './src/translator';
|
||||||
export {translateType} from './src/type_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';
|
export {translateExpression, translateStatement} from './src/typescript_translator';
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import * as o from '@angular/compiler';
|
import * as o from '@angular/compiler';
|
||||||
|
import {createTaggedTemplate} from 'typescript';
|
||||||
|
|
||||||
import {AstFactory, BinaryOperator, ObjectLiteralProperty, SourceMapRange, TemplateElement, TemplateLiteral, UnaryOperator} from './api/ast_factory';
|
import {AstFactory, BinaryOperator, ObjectLiteralProperty, SourceMapRange, TemplateElement, TemplateLiteral, UnaryOperator} from './api/ast_factory';
|
||||||
import {ImportGenerator} from './api/import_generator';
|
import {ImportGenerator} from './api/import_generator';
|
||||||
@ -38,21 +39,21 @@ const BINARY_OPERATORS = new Map<o.BinaryOperator, BinaryOperator>([
|
|||||||
export type RecordWrappedNodeExprFn<TExpression> = (expr: TExpression) => void;
|
export type RecordWrappedNodeExprFn<TExpression> = (expr: TExpression) => void;
|
||||||
|
|
||||||
export interface TranslatorOptions<TExpression> {
|
export interface TranslatorOptions<TExpression> {
|
||||||
downlevelLocalizedStrings?: boolean;
|
downlevelTaggedTemplates?: boolean;
|
||||||
downlevelVariableDeclarations?: boolean;
|
downlevelVariableDeclarations?: boolean;
|
||||||
recordWrappedNodeExpr?: RecordWrappedNodeExprFn<TExpression>;
|
recordWrappedNodeExpr?: RecordWrappedNodeExprFn<TExpression>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExpressionTranslatorVisitor<TStatement, TExpression> implements o.ExpressionVisitor,
|
export class ExpressionTranslatorVisitor<TStatement, TExpression> implements o.ExpressionVisitor,
|
||||||
o.StatementVisitor {
|
o.StatementVisitor {
|
||||||
private downlevelLocalizedStrings: boolean;
|
private downlevelTaggedTemplates: boolean;
|
||||||
private downlevelVariableDeclarations: boolean;
|
private downlevelVariableDeclarations: boolean;
|
||||||
private recordWrappedNodeExpr: RecordWrappedNodeExprFn<TExpression>;
|
private recordWrappedNodeExpr: RecordWrappedNodeExprFn<TExpression>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private factory: AstFactory<TStatement, TExpression>,
|
private factory: AstFactory<TStatement, TExpression>,
|
||||||
private imports: ImportGenerator<TExpression>, options: TranslatorOptions<TExpression>) {
|
private imports: ImportGenerator<TExpression>, options: TranslatorOptions<TExpression>) {
|
||||||
this.downlevelLocalizedStrings = options.downlevelLocalizedStrings === true;
|
this.downlevelTaggedTemplates = options.downlevelTaggedTemplates === true;
|
||||||
this.downlevelVariableDeclarations = options.downlevelVariableDeclarations === true;
|
this.downlevelVariableDeclarations = options.downlevelVariableDeclarations === true;
|
||||||
this.recordWrappedNodeExpr = options.recordWrappedNodeExpr || (() => {});
|
this.recordWrappedNodeExpr = options.recordWrappedNodeExpr || (() => {});
|
||||||
}
|
}
|
||||||
@ -168,6 +169,19 @@ export class ExpressionTranslatorVisitor<TStatement, TExpression> implements o.E
|
|||||||
ast.sourceSpan);
|
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 {
|
visitInstantiateExpr(ast: o.InstantiateExpr, context: Context): TExpression {
|
||||||
return this.factory.createNewExpression(
|
return this.factory.createNewExpression(
|
||||||
ast.classExpr.visitExpression(this, context),
|
ast.classExpr.visitExpression(this, context),
|
||||||
@ -202,13 +216,14 @@ export class ExpressionTranslatorVisitor<TStatement, TExpression> implements o.E
|
|||||||
}
|
}
|
||||||
|
|
||||||
const localizeTag = this.factory.createIdentifier('$localize');
|
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.
|
private createTaggedTemplateExpression(tag: TExpression, template: TemplateLiteral<TExpression>):
|
||||||
const localizeCall = this.downlevelLocalizedStrings ?
|
TExpression {
|
||||||
this.createES5TaggedTemplateFunctionCall(localizeTag, {elements, expressions}) :
|
return this.downlevelTaggedTemplates ? this.createES5TaggedTemplateFunctionCall(tag, template) :
|
||||||
this.factory.createTaggedTemplate(localizeTag, {elements, expressions});
|
this.factory.createTaggedTemplate(tag, template);
|
||||||
|
|
||||||
return this.setSourceMapRange(localizeCall, ast.sourceSpan);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -98,6 +98,10 @@ export class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor
|
|||||||
throw new Error('Method not implemented.');
|
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 {
|
visitInstantiateExpr(ast: o.InstantiateExpr, context: Context): never {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 * as ts from 'typescript';
|
||||||
|
|
||||||
import {attachComments} from '../ngtsc/translator';
|
import {attachComments} from '../ngtsc/translator';
|
||||||
@ -544,6 +544,10 @@ export class NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
|
|||||||
expr.args.map(arg => arg.visitExpression(this, null))));
|
expr.args.map(arg => arg.visitExpression(this, null))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
visitTaggedTemplateExpr(expr: TaggedTemplateExpr): RecordedNode<ts.TaggedTemplateExpression> {
|
||||||
|
throw new Error('tagged templates are not supported in pre-ivy mode.');
|
||||||
|
}
|
||||||
|
|
||||||
visitInstantiateExpr(expr: InstantiateExpr): RecordedNode<ts.NewExpression> {
|
visitInstantiateExpr(expr: InstantiateExpr): RecordedNode<ts.NewExpression> {
|
||||||
return this.postProcess(
|
return this.postProcess(
|
||||||
expr,
|
expr,
|
||||||
|
@ -78,7 +78,7 @@ export * from './ml_parser/tags';
|
|||||||
export {LexerRange} from './ml_parser/lexer';
|
export {LexerRange} from './ml_parser/lexer';
|
||||||
export * from './ml_parser/xml_parser';
|
export * from './ml_parser/xml_parser';
|
||||||
export {NgModuleCompiler} from './ng_module_compiler';
|
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 {EmitterVisitorContext} from './output/abstract_emitter';
|
||||||
export {JitEvaluator} from './output/output_jit';
|
export {JitEvaluator} from './output/output_jit';
|
||||||
export * from './output/ts_emitter';
|
export * from './output/ts_emitter';
|
||||||
|
@ -324,6 +324,7 @@ class KeyVisitor implements o.ExpressionVisitor {
|
|||||||
visitWritePropExpr = invalid;
|
visitWritePropExpr = invalid;
|
||||||
visitInvokeMethodExpr = invalid;
|
visitInvokeMethodExpr = invalid;
|
||||||
visitInvokeFunctionExpr = invalid;
|
visitInvokeFunctionExpr = invalid;
|
||||||
|
visitTaggedTemplateExpr = invalid;
|
||||||
visitInstantiateExpr = invalid;
|
visitInstantiateExpr = invalid;
|
||||||
visitConditionalExpr = invalid;
|
visitConditionalExpr = invalid;
|
||||||
visitNotExpr = invalid;
|
visitNotExpr = invalid;
|
||||||
|
@ -344,6 +344,17 @@ export abstract class AbstractEmitterVisitor implements o.StatementVisitor, o.Ex
|
|||||||
ctx.print(expr, `)`);
|
ctx.print(expr, `)`);
|
||||||
return null;
|
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<any>, ctx: EmitterVisitorContext): any {
|
visitWrappedNodeExpr(ast: o.WrappedNodeExpr<any>, ctx: EmitterVisitorContext): any {
|
||||||
throw new Error('Abstract emitter cannot visit WrappedNodeExpr.');
|
throw new Error('Abstract emitter cannot visit WrappedNodeExpr.');
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,21 @@
|
|||||||
import {AbstractEmitterVisitor, CATCH_ERROR_VAR, CATCH_STACK_VAR, EmitterVisitorContext, escapeIdentifier} from './abstract_emitter';
|
import {AbstractEmitterVisitor, CATCH_ERROR_VAR, CATCH_STACK_VAR, EmitterVisitorContext, escapeIdentifier} from './abstract_emitter';
|
||||||
import * as o from './output_ast';
|
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 {
|
export abstract class AbstractJsEmitterVisitor extends AbstractEmitterVisitor {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(false);
|
super(false);
|
||||||
@ -115,6 +130,27 @@ export abstract class AbstractJsEmitterVisitor extends AbstractEmitterVisitor {
|
|||||||
}
|
}
|
||||||
return null;
|
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 {
|
visitFunctionExpr(ast: o.FunctionExpr, ctx: EmitterVisitorContext): any {
|
||||||
ctx.print(ast, `function${ast.name ? ' ' + ast.name : ''}(`);
|
ctx.print(ast, `function${ast.name ? ' ' + ast.name : ''}(`);
|
||||||
this._visitParams(ast.params, ctx);
|
this._visitParams(ast.params, ctx);
|
||||||
@ -161,19 +197,7 @@ export abstract class AbstractJsEmitterVisitor extends AbstractEmitterVisitor {
|
|||||||
// ```
|
// ```
|
||||||
// $localize(__makeTemplateObject(cooked, raw), expression1, expression2, ...);
|
// $localize(__makeTemplateObject(cooked, raw), expression1, expression2, ...);
|
||||||
// ```
|
// ```
|
||||||
//
|
ctx.print(ast, `$localize(${makeTemplateObjectPolyfill}(`);
|
||||||
// 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})(');
|
|
||||||
const parts = [ast.serializeI18nHead()];
|
const parts = [ast.serializeI18nHead()];
|
||||||
for (let i = 1; i < ast.messageParts.length; i++) {
|
for (let i = 1; i < ast.messageParts.length; i++) {
|
||||||
parts.push(ast.serializeI18nTemplatePart(i));
|
parts.push(ast.serializeI18nTemplatePart(i));
|
||||||
|
@ -126,20 +126,26 @@ export function nullSafeIsEquivalent<T extends {isEquivalent(other: T): boolean}
|
|||||||
return base.isEquivalent(other);
|
return base.isEquivalent(other);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function areAllEquivalent<T extends {isEquivalent(other: T): boolean}>(
|
function areAllEquivalentPredicate<T>(
|
||||||
base: T[], other: T[]) {
|
base: T[], other: T[], equivalentPredicate: (baseElement: T, otherElement: T) => boolean) {
|
||||||
const len = base.length;
|
const len = base.length;
|
||||||
if (len !== other.length) {
|
if (len !== other.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
if (!base[i].isEquivalent(other[i])) {
|
if (!equivalentPredicate(base[i], other[i])) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function areAllEquivalent<T extends {isEquivalent(other: T): boolean}>(
|
||||||
|
base: T[], other: T[]) {
|
||||||
|
return areAllEquivalentPredicate(
|
||||||
|
base, other, (baseElement: T, otherElement: T) => baseElement.isEquivalent(otherElement));
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class Expression {
|
export abstract class Expression {
|
||||||
public type: Type|null;
|
public type: Type|null;
|
||||||
public sourceSpan: ParseSourceSpan|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 {
|
export class InstantiateExpr extends Expression {
|
||||||
constructor(
|
constructor(
|
||||||
public classExpr: Expression, public args: Expression[], type?: Type|null,
|
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 {
|
export abstract class MessagePiece {
|
||||||
constructor(public text: string, public sourceSpan: ParseSourceSpan) {}
|
constructor(public text: string, public sourceSpan: ParseSourceSpan) {}
|
||||||
}
|
}
|
||||||
@ -603,7 +650,7 @@ export interface CookedRawString {
|
|||||||
const escapeSlashes = (str: string): string => str.replace(/\\/g, '\\\\');
|
const escapeSlashes = (str: string): string => str.replace(/\\/g, '\\\\');
|
||||||
const escapeStartingColon = (str: string): string => str.replace(/^:/, '\\:');
|
const escapeStartingColon = (str: string): string => str.replace(/^:/, '\\:');
|
||||||
const escapeColons = (str: string): string => str.replace(/:/g, '\\:');
|
const escapeColons = (str: string): string => str.replace(/:/g, '\\:');
|
||||||
const escapeForMessagePart = (str: string): string =>
|
const escapeForTemplateLiteral = (str: string): string =>
|
||||||
str.replace(/`/g, '\\`').replace(/\${/g, '$\\{');
|
str.replace(/`/g, '\\`').replace(/\${/g, '$\\{');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -625,13 +672,13 @@ function createCookedRawString(
|
|||||||
if (metaBlock === '') {
|
if (metaBlock === '') {
|
||||||
return {
|
return {
|
||||||
cooked: messagePart,
|
cooked: messagePart,
|
||||||
raw: escapeForMessagePart(escapeStartingColon(escapeSlashes(messagePart))),
|
raw: escapeForTemplateLiteral(escapeStartingColon(escapeSlashes(messagePart))),
|
||||||
range,
|
range,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
cooked: `:${metaBlock}:${messagePart}`,
|
cooked: `:${metaBlock}:${messagePart}`,
|
||||||
raw: escapeForMessagePart(
|
raw: escapeForTemplateLiteral(
|
||||||
`:${escapeColons(escapeSlashes(metaBlock))}:${escapeSlashes(messagePart)}`),
|
`:${escapeColons(escapeSlashes(metaBlock))}:${escapeSlashes(messagePart)}`),
|
||||||
range,
|
range,
|
||||||
};
|
};
|
||||||
@ -953,6 +1000,7 @@ export interface ExpressionVisitor {
|
|||||||
visitWritePropExpr(expr: WritePropExpr, context: any): any;
|
visitWritePropExpr(expr: WritePropExpr, context: any): any;
|
||||||
visitInvokeMethodExpr(ast: InvokeMethodExpr, context: any): any;
|
visitInvokeMethodExpr(ast: InvokeMethodExpr, context: any): any;
|
||||||
visitInvokeFunctionExpr(ast: InvokeFunctionExpr, context: any): any;
|
visitInvokeFunctionExpr(ast: InvokeFunctionExpr, context: any): any;
|
||||||
|
visitTaggedTemplateExpr(ast: TaggedTemplateExpr, context: any): any;
|
||||||
visitInstantiateExpr(ast: InstantiateExpr, context: any): any;
|
visitInstantiateExpr(ast: InstantiateExpr, context: any): any;
|
||||||
visitLiteralExpr(ast: LiteralExpr, context: any): any;
|
visitLiteralExpr(ast: LiteralExpr, context: any): any;
|
||||||
visitLocalizedString(ast: LocalizedString, context: any): any;
|
visitLocalizedString(ast: LocalizedString, context: any): any;
|
||||||
@ -1275,6 +1323,17 @@ export class AstTransformer implements StatementVisitor, ExpressionVisitor {
|
|||||||
context);
|
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 {
|
visitInstantiateExpr(ast: InstantiateExpr, context: any): any {
|
||||||
return this.transformExpr(
|
return this.transformExpr(
|
||||||
new InstantiateExpr(
|
new InstantiateExpr(
|
||||||
@ -1378,7 +1437,7 @@ export class AstTransformer implements StatementVisitor, ExpressionVisitor {
|
|||||||
return this.transformExpr(
|
return this.transformExpr(
|
||||||
new CommaExpr(this.visitAllExpressions(ast.parts, context), ast.sourceSpan), context);
|
new CommaExpr(this.visitAllExpressions(ast.parts, context), ast.sourceSpan), context);
|
||||||
}
|
}
|
||||||
visitAllExpressions(exprs: Expression[], context: any): Expression[] {
|
visitAllExpressions<T extends Expression>(exprs: T[], context: any): T[] {
|
||||||
return exprs.map(expr => expr.visitExpression(this, context));
|
return exprs.map(expr => expr.visitExpression(this, context));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1524,6 +1583,11 @@ export class RecursiveAstVisitor implements StatementVisitor, ExpressionVisitor
|
|||||||
this.visitAllExpressions(ast.args, context);
|
this.visitAllExpressions(ast.args, context);
|
||||||
return this.visitExpression(ast, 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 {
|
visitInstantiateExpr(ast: InstantiateExpr, context: any): any {
|
||||||
ast.classExpr.visitExpression(this, context);
|
ast.classExpr.visitExpression(this, context);
|
||||||
this.visitAllExpressions(ast.args, context);
|
this.visitAllExpressions(ast.args, context);
|
||||||
@ -1808,6 +1872,12 @@ export function ifStmt(
|
|||||||
return new IfStmt(condition, thenClause, elseClause, sourceSpan, leadingComments);
|
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(
|
export function literal(
|
||||||
value: any, type?: Type|null, sourceSpan?: ParseSourceSpan|null): LiteralExpr {
|
value: any, type?: Type|null, sourceSpan?: ParseSourceSpan|null): LiteralExpr {
|
||||||
return new LiteralExpr(value, type, sourceSpan);
|
return new LiteralExpr(value, type, sourceSpan);
|
||||||
|
@ -197,6 +197,15 @@ class StatementInterpreter implements o.StatementVisitor, o.ExpressionVisitor {
|
|||||||
return fn.apply(null, args);
|
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 {
|
visitReturnStmt(stmt: o.ReturnStatement, ctx: _ExecutionContext): any {
|
||||||
return new ReturnValue(stmt.value.visitExpression(this, ctx));
|
return new ReturnValue(stmt.value.visitExpression(this, ctx));
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user