Skip to content

Commit 76a865b

Browse files
authored
feat: support TemplateExpression (#325)
1 parent 4298015 commit 76a865b

File tree

2 files changed

+102
-31
lines changed

2 files changed

+102
-31
lines changed

src/transform-node.ts

Lines changed: 100 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ function isImplicitThis(node: angular.AST, text: string): boolean {
3434
return start >= end || /^\s+$/.test(text.slice(start, end));
3535
}
3636

37+
type NodeTransformOptions = {
38+
isInParentParens?: boolean;
39+
parent?: angular.AST;
40+
};
41+
3742
class Transformer extends Source {
3843
#node;
3944
#text;
@@ -105,12 +110,19 @@ class Transformer extends Source {
105110
);
106111
}
107112

108-
#transform<T extends NGNode>(node: angular.AST, isInParentParens = false) {
109-
return this.#transformNode(node, isInParentParens) as T &
110-
LocationInformation;
113+
#transform<T extends NGNode>(
114+
node: angular.AST,
115+
options?: NodeTransformOptions,
116+
) {
117+
return this.#transformNode(node, options) as T & LocationInformation;
111118
}
112119

113-
#transformNode(node: angular.AST, isInParentParens = false): NGNode {
120+
#transformNode(node: angular.AST, options?: NodeTransformOptions): NGNode {
121+
const { isInParentParens } = {
122+
isInParentParens: false,
123+
...options,
124+
};
125+
114126
if (node instanceof angular.Interpolation) {
115127
const { expressions } = node;
116128

@@ -374,7 +386,11 @@ class Transformer extends Source {
374386
const { receiver, args } = node;
375387
const tArgs =
376388
args.length === 1
377-
? [this.#transform<babel.Expression>(args[0], true)]
389+
? [
390+
this.#transform<babel.Expression>(args[0], {
391+
isInParentParens: true,
392+
}),
393+
]
378394
: (args as angular.AST[]).map<babel.Expression>((node) =>
379395
this.#transform(node),
380396
);
@@ -392,7 +408,7 @@ class Transformer extends Source {
392408
optional:
393409
nodeType === 'OptionalCallExpression' ? isOptionalType : undefined,
394410
start: getOuterStart(tReceiver),
395-
end: node.sourceSpan.end, // )
411+
end: node.sourceSpan.end, // `)`
396412
},
397413
{ hasParentParens: isInParentParens },
398414
);
@@ -405,7 +421,7 @@ class Transformer extends Source {
405421
type: 'TSNonNullExpression',
406422
expression: expression,
407423
start: getOuterStart(expression),
408-
end: node.sourceSpan.end, // !
424+
end: node.sourceSpan.end, // `!`
409425
},
410426
{ hasParentParens: isInParentParens },
411427
);
@@ -523,35 +539,88 @@ class Transformer extends Source {
523539
);
524540
}
525541

542+
if (node instanceof angular.TemplateLiteral) {
543+
const { elements, expressions } = node;
544+
545+
return this.#create<babel.TemplateLiteral>({
546+
type: 'TemplateLiteral',
547+
quasis: elements.map((element) =>
548+
this.#transform(element, { parent: node }),
549+
),
550+
expressions: expressions.map((expression) =>
551+
this.#transform(expression),
552+
),
553+
...node.sourceSpan,
554+
});
555+
}
556+
557+
if (node instanceof angular.TemplateLiteralElement) {
558+
const templateLiteral = options!.parent! as angular.TemplateLiteral;
559+
const elementIndex = templateLiteral.elements.indexOf(node);
560+
const isFirst = elementIndex === 0;
561+
const isLast = elementIndex === templateLiteral.elements.length - 1;
562+
563+
// The `TemplateLiteralElement` don't have correct location information
564+
const start = isFirst
565+
? templateLiteral.sourceSpan.start + 1
566+
: node.sourceSpan.start;
567+
let end;
568+
if (isLast) {
569+
end = templateLiteral.sourceSpan.end - 1;
570+
} else {
571+
const nextExpression = templateLiteral.expressions[elementIndex];
572+
// TODO: Support search multiple characters in `getCharacterLastIndex()`
573+
// FIXME: Search `${` instead
574+
end = this.getCharacterLastIndex('$', nextExpression.sourceSpan.start);
575+
}
576+
const raw = this.text.slice(start, end);
577+
578+
return this.#create<babel.TemplateElement>(
579+
{
580+
type: 'TemplateElement',
581+
value: {
582+
cooked: node.text,
583+
raw,
584+
},
585+
start: start,
586+
end: end,
587+
tail: isLast,
588+
},
589+
{ stripSpaces: false },
590+
);
591+
}
592+
526593
// istanbul ignore next
527-
throw Object.assign(new Error('Unexpected node'), { node });
594+
throw new Error(`Unexpected node type '${node.constructor.name}'`);
528595
}
529596
}
530597

531598
// See `convertAst` in `@angular/compiler`
532-
// ASTWithSource (Not handled)
533-
// PropertyRead
534-
// PropertyWrite
535-
// KeyedWrite
536-
// Call
537-
// LiteralPrimitive
538-
// Unary
539-
// Binary
540-
// ThisReceiver (Not handled)
541-
// KeyedRead
542-
// Chain
543-
// LiteralMap
544-
// LiteralArray
545-
// Conditional
546-
// NonNullAssert
547-
// BindingPipe
548-
// SafeKeyedRead
549-
// SafePropertyRead
550-
// SafeCall
551-
// EmptyExpr
552-
// PrefixNot
553-
// TypeofExpression
554-
function transform(node: angular.AST, text: string): NGNode {
599+
type SupportedNodes =
600+
| angular.ASTWithSource // Not handled
601+
| angular.PropertyRead
602+
| angular.PropertyWrite
603+
| angular.KeyedWrite
604+
| angular.Call
605+
| angular.LiteralPrimitive
606+
| angular.Unary
607+
| angular.Binary
608+
| angular.ThisReceiver // Not handled
609+
| angular.KeyedRead
610+
| angular.Chain
611+
| angular.LiteralMap
612+
| angular.LiteralArray
613+
| angular.Conditional
614+
| angular.NonNullAssert
615+
| angular.BindingPipe
616+
| angular.SafeKeyedRead
617+
| angular.SafePropertyRead
618+
| angular.SafeCall
619+
| angular.EmptyExpr
620+
| angular.PrefixNot
621+
| angular.TypeofExpression
622+
| angular.TemplateLiteral; // Including `TemplateLiteralElement`
623+
function transform(node: SupportedNodes, text: string): NGNode {
555624
return new Transformer(node, text).node;
556625
}
557626

tests/transform.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ describe.each`
9494
${'TypeofExpression'} | ${'UnaryExpression'} | ${' typeof {} '} | ${true} | ${true} | ${true} | ${true}
9595
${'Binary'} | ${'BinaryExpression'} | ${' typeof {} === "object" '} | ${true} | ${true} | ${true} | ${true}
9696
${'PrefixNot'} | ${'UnaryExpression'} | ${' ! ( typeof {} === "number" ) '} | ${true} | ${true} | ${true} | ${true}
97+
${'TemplateLiteral'} | ${'TemplateLiteral'} | ${' ` a ${ b } \u0063 ` '} | ${true} | ${true} | ${true} | ${true}
98+
${'TemplateLiteral'} | ${'TemplateLiteral'} | ${' ( ` a ${ b } \u0063 ` ) '} | ${true} | ${true} | ${true} | ${true}
9799
`('($expectedAngularType -> $expectedEstreeType)', (fields) => {
98100
for (const method of PARSE_METHODS) {
99101
testSection(method, fields);

0 commit comments

Comments
 (0)