From 03092f129a9eeb962a12f7b33c06cf08763bc63a Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Thu, 22 Aug 2024 00:16:52 +0530 Subject: [PATCH] feat: add support for template strings --- readme.md | 10 ++++++ src/lexer.ts | 6 +++- src/parser.ts | 40 +++++++++++++++++++++ src/reverse_translator.ts | 16 +++++++++ src/translator.ts | 16 +++++++++ src/types.ts | 6 ++-- test/scenarios/template_strings/data.ts | 17 +++++++++ test/scenarios/template_strings/template.jt | 3 ++ 8 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 test/scenarios/template_strings/data.ts create mode 100644 test/scenarios/template_strings/template.jt diff --git a/readme.md b/readme.md index 330da63..39e0791 100644 --- a/readme.md +++ b/readme.md @@ -96,6 +96,7 @@ Give the JSON template engine a try in our [playground](https://transformers-wor The template consists of multiple statements, with the output being the result of the final statement. + ### Variables ```js @@ -106,6 +107,15 @@ a + b; Refer this [example](test/scenarios/assignments/template.jt) for more details. +#### Template Strings + +```js +let a = `Input a=${.a}`; +let b = `Input b=${.b}`; +`${a}, ${b}`; +``` +Refer this [example](test/scenarios/template_strings/template.jt) for more details. + ### Basic Expressions #### Conditions diff --git a/src/lexer.ts b/src/lexer.ts index 97a9d58..f0e96b6 100644 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -61,6 +61,10 @@ export class JsonTemplateLexer { return JsonTemplateLexer.isLiteralToken(this.lookahead()); } + matchTemplate(): boolean { + return this.matchTokenType(TokenType.TEMPLATE); + } + matchINT(steps = 0): boolean { return this.matchTokenType(TokenType.INT, steps); } @@ -463,7 +467,7 @@ export class JsonTemplateLexer { if (eosFound) { return { - type: TokenType.STR, + type: orig === '`' ? TokenType.TEMPLATE : TokenType.STR, value: str, range: [start, this.idx], }; diff --git a/src/parser.ts b/src/parser.ts index af77631..5661d35 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -37,6 +37,7 @@ import { SpreadExpression, StatementsExpression, SyntaxType, + TemplateExpression, ThrowExpression, Token, TokenType, @@ -973,6 +974,40 @@ export class JsonTemplateParser { return JsonTemplateParser.createLiteralExpr(this.lexer.lex()); } + private parseTemplateExpr(): TemplateExpression { + const template = this.lexer.value() as string; + let idx = 0; + const parts: Expression[] = []; + while (idx < template.length) { + const start = template.indexOf('${', idx); + if (start === -1) { + parts.push({ + type: SyntaxType.LITERAL, + value: template.slice(idx), + tokenType: TokenType.STR, + }); + break; + } + const end = template.indexOf('}', start); + if (end === -1) { + throw new JsonTemplateParserError('Invalid template expression'); + } + if (start > idx) { + parts.push({ + type: SyntaxType.LITERAL, + value: template.slice(idx, start), + tokenType: TokenType.STR, + }); + } + parts.push(JsonTemplateEngine.parse(template.slice(start + 2, end), this.options)); + idx = end + 1; + } + return { + type: SyntaxType.TEMPLATE_EXPR, + parts, + }; + } + private parseIDPath(): string { const idParts: string[] = []; while (this.lexer.matchID()) { @@ -1380,6 +1415,7 @@ export class JsonTemplateParser { return JsonTemplateEngine.parseMappingPaths(flatMappings); } + // eslint-disable-next-line sonarjs/cognitive-complexity private parsePrimaryExpr(): Expression { if (this.lexer.match(';')) { return EMPTY_EXPR; @@ -1412,6 +1448,10 @@ export class JsonTemplateParser { return this.parseLiteralExpr(); } + if (this.lexer.matchTemplate()) { + return this.parseTemplateExpr(); + } + if (this.lexer.matchCompileTimeExpr()) { return this.parseCompileTimeExpr(); } diff --git a/src/reverse_translator.ts b/src/reverse_translator.ts index a35294e..ccaddd8 100644 --- a/src/reverse_translator.ts +++ b/src/reverse_translator.ts @@ -28,6 +28,7 @@ import { SpreadExpression, StatementsExpression, SyntaxType, + TemplateExpression, ThrowExpression, TokenType, UnaryExpression, @@ -69,6 +70,9 @@ export class JsonTemplateReverseTranslator { return this.translateBinaryExpression(expr as BinaryExpression); case SyntaxType.ARRAY_EXPR: return this.translateArrayExpression(expr as ArrayExpression); + case SyntaxType.TEMPLATE_EXPR: + return this.translateTemplateExpression(expr as TemplateExpression); + case SyntaxType.OBJECT_EXPR: return this.translateObjectExpression(expr as ObjectExpression); case SyntaxType.SPREAD_EXPR: @@ -120,6 +124,18 @@ export class JsonTemplateReverseTranslator { } } + translateTemplateExpression(expr: TemplateExpression): string { + const code: string[] = []; + for (const part of expr.parts) { + if (part.type === SyntaxType.LITERAL) { + code.push(part.value); + } else { + code.push(this.translateWithWrapper(part, '${', '}')); + } + } + return `\`${code.join('')}\``; + } + translateArrayFilterExpression(expr: ArrayFilterExpression): string { return this.translateExpression(expr.filter); } diff --git a/src/translator.ts b/src/translator.ts index bd4ce61..955d17b 100644 --- a/src/translator.ts +++ b/src/translator.ts @@ -40,6 +40,7 @@ import { IncrementExpression, LoopControlExpression, ObjectPropExpression, + TemplateExpression, } from './types'; import { convertToStatementsExpr, escapeStr } from './utils/common'; import { translateLiteral } from './utils/translator'; @@ -141,6 +142,9 @@ export class JsonTemplateTranslator { case SyntaxType.LITERAL: return this.translateLiteralExpr(expr as LiteralExpression, dest, ctx); + case SyntaxType.TEMPLATE_EXPR: + return this.translateTemplateExpr(expr as TemplateExpression, dest, ctx); + case SyntaxType.ARRAY_EXPR: return this.translateArrayExpr(expr as ArrayExpression, dest, ctx); @@ -626,6 +630,18 @@ export class JsonTemplateTranslator { return JsonTemplateTranslator.generateAssignmentCode(dest, literalCode); } + private translateTemplateExpr(expr: TemplateExpression, dest: string, ctx: string): string { + const code: string[] = []; + const partVars: string[] = []; + for (const part of expr.parts) { + const partVar = this.acquireVar(); + code.push(this.translateExpr(part, partVar, ctx)); + partVars.push(partVar); + } + code.push(JsonTemplateTranslator.generateAssignmentCode(dest, partVars.join(' + '))); + return code.join(''); + } + private getSimplePathSelector(expr: SelectorExpression, isAssignment: boolean): string { if (expr.prop?.type === TokenType.STR) { return `${isAssignment ? '' : '?.'}[${escapeStr(expr.prop?.value)}]`; diff --git a/src/types.ts b/src/types.ts index fd08823..f694f2b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -28,6 +28,7 @@ export enum TokenType { ID = 'id', INT = 'int', FLOAT = 'float', + TEMPLATE = 'template', STR = 'str', BOOL = 'bool', NULL = 'null', @@ -95,6 +96,7 @@ export enum SyntaxType { STATEMENTS_EXPR = 'statements_expr', LOOP_CONTROL_EXPR = 'loop_control_expr', LOOP_EXPR = 'loop_expr', + TEMPLATE_EXPR = 'TEMPLATE_EXPR', } export enum PathType { @@ -176,8 +178,8 @@ export interface BinaryExpression extends Expression { op: string; } -export interface ConcatExpression extends Expression { - args: Expression[]; +export interface TemplateExpression extends Expression { + parts: Expression[]; } export interface AssignmentExpression extends Expression { diff --git a/test/scenarios/template_strings/data.ts b/test/scenarios/template_strings/data.ts new file mode 100644 index 0000000..3196109 --- /dev/null +++ b/test/scenarios/template_strings/data.ts @@ -0,0 +1,17 @@ +import { Scenario } from '../../types'; + +export const data: Scenario[] = [ + { + input: { + a: 'foo', + }, + bindings: { + b: 'bar', + }, + output: 'Input a=foo, Binding b=bar', + }, + { + template: '`unclosed template ${`', + error: 'Invalid template expression', + }, +]; diff --git a/test/scenarios/template_strings/template.jt b/test/scenarios/template_strings/template.jt new file mode 100644 index 0000000..25152c4 --- /dev/null +++ b/test/scenarios/template_strings/template.jt @@ -0,0 +1,3 @@ +let a = `Input a=${.a}`; +let b = `Binding b=${$.b}`; +`${a}, ${b}`; \ No newline at end of file