Skip to content

Commit 323053d

Browse files
authored
[Expression]add string interpolation feature (#1759)
1 parent a5eb5b2 commit 323053d

File tree

19 files changed

+1371
-739
lines changed

19 files changed

+1371
-739
lines changed

libraries/adaptive-expressions/src/expressionFunctions.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2077,9 +2077,18 @@ export class ExpressionFunctions {
20772077
ExpressionFunctions.validateUnary),
20782078
new ExpressionEvaluator(
20792079
ExpressionType.Concat,
2080-
ExpressionFunctions.apply((args: any []): string => ''.concat(...args.map((arg): string => ExpressionFunctions.parseStringOrNull(arg))), ExpressionFunctions.verifyStringOrNull),
2080+
ExpressionFunctions.apply((args: any []): string => {
2081+
let result = '';
2082+
for (const arg of args) {
2083+
if (arg !== undefined && arg !== null) {
2084+
result += arg.toString();
2085+
}
2086+
}
2087+
2088+
return result;
2089+
}),
20812090
ReturnType.String,
2082-
ExpressionFunctions.validateString),
2091+
ExpressionFunctions.validateAtLeastOne),
20832092
new ExpressionEvaluator(
20842093
ExpressionType.Length,
20852094
ExpressionFunctions.apply((args: any []): number => (ExpressionFunctions.parseStringOrNull(args[0])).length, ExpressionFunctions.verifyStringOrNull),
@@ -2936,7 +2945,7 @@ export class ExpressionFunctions {
29362945
lookup.set('lessOrEquals', lookup.get(ExpressionType.LessThanOrEqual));
29372946
lookup.set('not', lookup.get(ExpressionType.Not));
29382947
lookup.set('or', lookup.get(ExpressionType.Or));
2939-
lookup.set('concat', lookup.get(ExpressionType.Concat));
2948+
lookup.set('&', lookup.get(ExpressionType.Concat));
29402949

29412950
return lookup;
29422951
}

libraries/adaptive-expressions/src/expressionType.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export class ExpressionType {
4141
public static readonly Not: string = '!';
4242

4343
// String
44-
public static readonly Concat: string = '&';
44+
public static readonly Concat: string = 'concat';
4545
public static readonly Length: string = 'length';
4646
public static readonly Replace: string = 'replace';
4747
public static readonly ReplaceIgnoreCase: string = 'replaceIgnoreCase';

libraries/adaptive-expressions/src/parser/Expression.g4

Lines changed: 0 additions & 48 deletions
This file was deleted.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
lexer grammar ExpressionLexer;
2+
3+
@lexer::members {
4+
ignoreWS = true; // usually we ignore whitespace, but inside stringInterpolation, whitespace is significant
5+
}
6+
7+
fragment LETTER : [a-zA-Z];
8+
fragment DIGIT : [0-9];
9+
10+
STRING_INTERPOLATION_START : '`' { this.ignoreWS = false;} -> pushMode(STRING_INTERPOLATION_MODE);
11+
12+
// operators
13+
PLUS: '+';
14+
15+
SUBSTRACT: '-';
16+
17+
NON: '!';
18+
19+
XOR: '^';
20+
21+
ASTERISK: '*';
22+
23+
SLASH: '/';
24+
25+
PERCENT: '%';
26+
27+
DOUBLE_EQUAL: '==';
28+
29+
NOT_EQUAL: '!=' | '<>';
30+
31+
SINGLE_AND: '&';
32+
33+
DOUBLE_AND: '&&';
34+
35+
DOUBLE_VERTICAL_CYLINDER: '||';
36+
37+
LESS_THAN: '<';
38+
39+
MORE_THAN: '>';
40+
41+
LESS_OR_EQUAl: '<=';
42+
43+
MORE_OR_EQUAL: '>=';
44+
45+
OPEN_BRACKET: '(';
46+
47+
CLOSE_BRACKET: ')';
48+
49+
DOT: '.';
50+
51+
OPEN_SQUARE_BRACKET: '[';
52+
53+
CLOSE_SQUARE_BRACKET: ']';
54+
55+
COMMA: ',';
56+
57+
58+
NUMBER : DIGIT + ( '.' DIGIT +)? ;
59+
60+
WHITESPACE : (' '|'\t'|'\ufeff'|'\u00a0') {this.ignoreWS}? -> skip;
61+
62+
IDENTIFIER : (LETTER | '_' | '#' | '@' | '@@' | '$' | '%') (LETTER | DIGIT | '-' | '_')* '!'?;
63+
64+
NEWLINE : '\r'? '\n' -> skip;
65+
66+
STRING : ('\'' (~'\'')* '\'') | ('"' (~'"')* '"');
67+
68+
CONSTANT : ('[' WHITESPACE* ']') | ('{' WHITESPACE* '}');
69+
70+
INVALID_TOKEN_DEFAULT_MODE : . ;
71+
72+
mode STRING_INTERPOLATION_MODE;
73+
74+
STRING_INTERPOLATION_END : '`' {this.ignoreWS = true;} -> type(STRING_INTERPOLATION_START), popMode;
75+
76+
TEMPLATE : '$' '{' (STRING | ~[\r\n{}'"])*? '}';
77+
78+
ESCAPE_CHARACTER : '\\' ~[\r\n]?;
79+
80+
TEXT_CONTENT : '\\`' | ~[\r\n];
81+
82+
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
parser grammar ExpressionParser;
2+
3+
options { tokenVocab=ExpressionLexer; }
4+
5+
file: expression EOF;
6+
7+
expression
8+
: (NON|SUBSTRACT|PLUS) expression #unaryOpExp
9+
| <assoc=right> expression XOR expression #binaryOpExp
10+
| expression (ASTERISK|SLASH|PERCENT) expression #binaryOpExp
11+
| expression (PLUS|SUBSTRACT) expression #binaryOpExp
12+
| expression (DOUBLE_EQUAL|NOT_EQUAL) expression #binaryOpExp
13+
| expression (SINGLE_AND) expression #binaryOpExp
14+
| expression (LESS_THAN|LESS_OR_EQUAl|MORE_THAN|MORE_OR_EQUAL) expression #binaryOpExp
15+
| expression DOUBLE_AND expression #binaryOpExp
16+
| expression DOUBLE_VERTICAL_CYLINDER expression #binaryOpExp
17+
| primaryExpression #primaryExp
18+
;
19+
20+
primaryExpression
21+
: OPEN_BRACKET expression CLOSE_BRACKET #parenthesisExp
22+
| CONSTANT #constantAtom
23+
| NUMBER #numericAtom
24+
| STRING #stringAtom
25+
| IDENTIFIER #idAtom
26+
| stringInterpolation #stringInterpolationAtom
27+
| primaryExpression DOT IDENTIFIER #memberAccessExp
28+
| primaryExpression OPEN_BRACKET argsList? CLOSE_BRACKET #funcInvokeExp
29+
| primaryExpression OPEN_SQUARE_BRACKET expression CLOSE_SQUARE_BRACKET #indexAccessExp
30+
;
31+
32+
stringInterpolation
33+
: STRING_INTERPOLATION_START (ESCAPE_CHARACTER | TEMPLATE | textContent)+ STRING_INTERPOLATION_START
34+
;
35+
36+
textContent
37+
: TEXT_CONTENT+
38+
;
39+
40+
argsList
41+
: expression (COMMA expression)*
42+
;

libraries/adaptive-expressions/src/parser/expressionEngine.ts

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
*/
99
import { ANTLRInputStream, CommonTokenStream } from 'antlr4ts';
1010
// tslint:disable-next-line: no-submodule-imports
11-
import { AbstractParseTreeVisitor, ParseTree } from 'antlr4ts/tree';
11+
import { AbstractParseTreeVisitor, ParseTree, TerminalNode } from 'antlr4ts/tree';
1212
import { ExpressionFunctions } from '../expressionFunctions';
1313
import { Constant } from '../constant';
1414
import { Expression } from '../expression';
1515
import { EvaluatorLookup } from '../expressionEvaluator';
1616
import { ExpressionParserInterface } from '../expressionParser';
1717
import { ExpressionType } from '../expressionType';
18-
import { ExpressionLexer, ExpressionParser, ExpressionVisitor } from './generated';
18+
import { ExpressionLexer, ExpressionParser, ExpressionParserVisitor } from './generated';
1919
import * as ep from './generated/ExpressionParser';
2020
import { ParseErrorListener } from './parseErrorListener';
2121
import { Util } from './util';
@@ -30,7 +30,7 @@ export class ExpressionEngine implements ExpressionParserInterface {
3030
public readonly EvaluatorLookup: EvaluatorLookup;
3131

3232
// tslint:disable-next-line: typedef
33-
private readonly ExpressionTransformer = class extends AbstractParseTreeVisitor<Expression> implements ExpressionVisitor<Expression> {
33+
private readonly ExpressionTransformer = class extends AbstractParseTreeVisitor<Expression> implements ExpressionParserVisitor<Expression> {
3434

3535
private readonly _lookup: EvaluatorLookup = undefined;
3636
public constructor(lookup: EvaluatorLookup) {
@@ -120,6 +120,35 @@ export class ExpressionEngine implements ExpressionParserInterface {
120120
}
121121
}
122122

123+
public visitStringInterpolationAtom(context: ep.StringInterpolationAtomContext): Expression {
124+
let children: Expression[] = [];
125+
126+
for (const node of context.stringInterpolation().children) {
127+
if (node instanceof TerminalNode){
128+
switch((node as TerminalNode).symbol.type) {
129+
case ep.ExpressionParser.TEMPLATE:
130+
const expressionString = this.trimExpression(node.text);
131+
children.push(new ExpressionEngine(this._lookup).parse(expressionString));
132+
break;
133+
case ep.ExpressionParser.TEXT_CONTENT:
134+
children.push(new Constant(node.text));
135+
break;
136+
case ep.ExpressionParser.ESCAPE_CHARACTER:
137+
children.push(new Constant(Util.unescape(node.text)));
138+
break;
139+
default:
140+
break;
141+
}
142+
} else {
143+
children.push(new Constant(node.text));
144+
}
145+
146+
}
147+
148+
return this.MakeExpression(ExpressionType.Concat, ...children);
149+
150+
}
151+
123152
public visitConstantAtom(context: ep.ConstantAtomContext): Expression {
124153
let text: string = context.text;
125154
if (text.startsWith('[') && text.endsWith(']')) {
@@ -154,6 +183,23 @@ export class ExpressionEngine implements ExpressionParserInterface {
154183

155184
return result;
156185
}
186+
187+
private trimExpression(expression: string): string {
188+
let result = expression.trim();
189+
if (result.startsWith('$')) {
190+
result = result.substr(1);
191+
}
192+
193+
result = result.trim();
194+
195+
if (result.startsWith('{') && result.endsWith('}')) {
196+
result = result.substr(1, result.length - 2);
197+
}
198+
199+
return result.trim();
200+
}
201+
202+
157203
};
158204

159205
public constructor(lookup?: EvaluatorLookup) {

0 commit comments

Comments
 (0)