Skip to content

Commit c5856c6

Browse files
authored
feat: improved converter and function call parsing mechanism for XML expressions (#9805)
1 parent b1640b0 commit c5856c6

2 files changed

Lines changed: 75 additions & 63 deletions

File tree

‎packages/core/ui/builder/binding-builder.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// regex that contains all symbols applicable for expression used to AI detect an expression.
2-
const expressionSymbolsRegex = /[\+\-\*\/%\?:<>=!\|&\(\)^~]|\$\{.+\}/;
2+
const expressionSymbolsRegex = /[\+\-\*\/%\?:<>=!\|&\(\)^~]|^`.*\$\{.+\}.*`$/;
33

44
export namespace bindingConstants {
55
export const sourceProperty = 'sourceProperty';

‎packages/core/ui/core/bindable/bindable-expressions.ts‎

Lines changed: 74 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const binaryOperators = {
2424
'+': (l, r) => l + r,
2525
'-': (l, r) => l - r,
2626
'*': (l, r) => l * r,
27+
'**': (l, r) => l ** r,
2728
'/': (l, r) => l / r,
2829
'%': (l, r) => l % r,
2930
'<': (l, r) => l < r,
@@ -63,54 +64,53 @@ const expressionParsers = {
6364

6465
const left = convertExpressionToValue(expression.left, model, isBackConvert, changedModel);
6566

66-
if (expression.operator == '|' && expression.right.type == 'CallExpression') {
67-
expression.right.requiresConverter = true;
67+
let converterExpression = expression.right;
68+
if (expression.operator == '|') {
69+
if (converterExpression.type == 'ChainExpression') {
70+
converterExpression = converterExpression.expression;
71+
}
72+
if (converterExpression.type == 'CallExpression') {
73+
!converterExpression.arguments.includes(expression.left) && converterExpression.arguments.unshift(expression.left);
74+
expression.right.nsIsCallable = true;
75+
converterExpression = converterExpression.callee;
76+
}
77+
78+
switch (converterExpression.type) {
79+
case 'Identifier':
80+
case 'MemberExpression':
81+
case 'NewExpression':
82+
converterExpression.nsRequiresConverter = true;
83+
converterExpression.nsIsPendingCall = true;
84+
break;
85+
default:
86+
throw new Error('Invalid converter syntax');
87+
}
6888
}
89+
6990
const right = convertExpressionToValue(expression.right, model, isBackConvert, changedModel);
7091

7192
if (expression.operator == '|') {
72-
if (expression.right.requiresConverter && right != null) {
73-
right.args.unshift(left);
74-
return right.callback.apply(right.context, right.args);
93+
if (converterExpression.nsRequiresConverter) {
94+
return expression.right.nsIsCallable ? right : right?.(left);
7595
}
76-
throw new Error('Invalid converter after ' + expression.operator + ' operator');
96+
throw new Error('Invalid converter syntax');
7797
}
7898
return binaryOperators[expression.operator](left, right);
7999
},
80100
'CallExpression': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
81-
expression.callee.requiresObjectAndProperty = true;
82-
83-
const { object, property } = convertExpressionToValue(expression.callee, model, isBackConvert, changedModel);
84-
85-
let callback;
86-
if (object == FORCED_CHAIN_VALUE) {
87-
callback = undefined;
88-
} else {
89-
callback = expression.callee.optional ? object?.[property] : object[property];
90-
}
101+
expression.callee.nsIsPendingCall = true;
102+
const callback = convertExpressionToValue(expression.callee, model, isBackConvert, changedModel);
91103

92-
if ((!expression.optional || expression.requiresConverter) && isNullOrUndefined(callback)) {
104+
if (!expression.optional && isNullOrUndefined(callback)) {
93105
throw new Error('Cannot perform a call using a null or undefined property');
94106
}
95107

96-
if (expression.requiresConverter) {
97-
if (isFunction(callback)) {
98-
callback = { toView: callback };
99-
} else if (!isObject(callback) || !isFunction(callback.toModel) && !isFunction(callback.toView)) {
100-
throw new Error('Invalid converter call');
101-
}
102-
}
103-
104108
const parsedArgs = [];
105109
for (let argument of expression.arguments) {
106110
let value = convertExpressionToValue(argument, model, isBackConvert, changedModel);
107111
argument.type == 'SpreadElement' ? parsedArgs.push(...value) : parsedArgs.push(value);
108112
}
109-
110-
if (expression.requiresConverter) {
111-
return getConverter(callback, object, parsedArgs, isBackConvert);
112-
}
113-
return expression.optional ? callback?.apply(object, parsedArgs) : callback.apply(object, parsedArgs);
113+
return expression.optional ? callback?.(...parsedArgs) : callback(...parsedArgs);
114114
},
115115
'ChainExpression': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
116116
return convertExpressionToValue(expression.expression, model, isBackConvert, changedModel);
@@ -121,10 +121,11 @@ const expressionParsers = {
121121
},
122122
'Identifier': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
123123
const context = getContext(expression.name, model, changedModel);
124-
if (expression.requiresObjectAndProperty) {
125-
return { object: context, property: expression.name };
126-
}
127-
return context[expression.name];
124+
let value = context[expression.name];
125+
if (expression.nsRequiresConverter) {
126+
value = getConverterCallback(value, isBackConvert);
127+
}
128+
return expression.nsIsPendingCall && typeof value === 'function' ? value.bind(context) : value;
128129
},
129130
'Literal': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
130131
return expression.regex != null ? new RegExp(expression.regex.pattern, expression.regex.flags) : expression.value;
@@ -138,34 +139,34 @@ const expressionParsers = {
138139
},
139140
'MemberExpression': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
140141
if (expression.object.type == 'MemberExpression') {
141-
expression.object.isChained = true;
142+
expression.object.nsIsChained = true;
142143
}
143144

144145
const object = convertExpressionToValue(expression.object, model, isBackConvert, changedModel);
145146
const property = expression.computed ? convertExpressionToValue(expression.property, model, isBackConvert, changedModel) : expression.property?.name;
146-
const propertyInfo = { object, property };
147-
148-
if (expression.requiresObjectAndProperty) {
149-
return propertyInfo;
150-
}
151147

152148
/**
153-
* If first member is undefined, make sure that no error is thrown later but return undefined instead.
154-
* This behaviour is kept in order to cope with components whose binding context takes a bit long to load.
155-
* Old parser would return undefined for an expression like 'property1.property2.property3'
156-
* even if expression as a whole consisted of undefined properties.
157-
* The new one will keep the same principle only if first member is undefined for safety reasons.
149+
* If an expression parent property is null or undefined, apply null-safety.
150+
* This behaviour also helps cope with components whose binding context takes a bit longer to load.
151+
* Old parser would be null-safe for properties and sub-properties
152+
* even if expression as a whole consisted of undefined ones.
153+
* The new parser will keep the same principle only if parent property is null or undefined, resulting in better control over code and errors.
158154
* It meddles with members specifically, so that it will not affect expression result as a whole.
159155
* For example, an 'isLoading || isBusy' expression will be validated as 'undefined || undefined'
160156
* if context is not ready.
161157
*/
162-
if (object === undefined && expression.object.type == 'Identifier') {
163-
return expression.isChained ? FORCED_CHAIN_VALUE : object;
158+
if (object == null && expression.object.type == 'Identifier') {
159+
return expression.nsIsChained ? FORCED_CHAIN_VALUE : undefined;
164160
}
165161
if (object == FORCED_CHAIN_VALUE) {
166-
return expression.isChained ? object : undefined;
162+
return expression.nsIsChained ? object : undefined;
163+
}
164+
165+
let value = expression.optional ? object?.[property] : object[property];
166+
if (expression.nsRequiresConverter) {
167+
value = getConverterCallback(value, isBackConvert);
167168
}
168-
return expression.optional ? object?.[property] : object[property];
169+
return expression.nsIsPendingCall && typeof value === 'function' ? value.bind(object) : value;
169170
},
170171
'NewExpression': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
171172
const callback = convertExpressionToValue(expression.callee, model, isBackConvert, changedModel);
@@ -174,7 +175,12 @@ const expressionParsers = {
174175
let value = convertExpressionToValue(argument, model, isBackConvert, changedModel);
175176
argument.type == 'SpreadElement' ? parsedArgs.push(...value) : parsedArgs.push(value);
176177
}
177-
return new callback(...parsedArgs);
178+
179+
let value = new callback(...parsedArgs);
180+
if (expression.nsRequiresConverter) {
181+
value = getConverterCallback(value, isBackConvert);
182+
}
183+
return value;
178184
},
179185
'ObjectExpression': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
180186
const parsedObject = {};
@@ -198,12 +204,14 @@ const expressionParsers = {
198204
},
199205
'TemplateLiteral': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
200206
let parsedText = '';
201-
for (let q of expression.quasis) {
202-
parsedText += convertExpressionToValue(q, model, isBackConvert, changedModel);
203-
}
207+
const length = expression.quasis.length;
204208

205-
for (let ex of expression.expressions) {
206-
parsedText += convertExpressionToValue(ex, model, isBackConvert, changedModel);
209+
for (let i = 0; i < length; i++) {
210+
let q = expression.quasis[i];
211+
parsedText += convertExpressionToValue(q, model, isBackConvert, changedModel);
212+
if (!q.tail) {
213+
parsedText += convertExpressionToValue(expression.expressions[i], model, isBackConvert, changedModel);
214+
}
207215
}
208216
return parsedText;
209217
},
@@ -225,14 +233,18 @@ function getContext(key, model, changedModel) {
225233
return context;
226234
}
227235

228-
function getConverter(converterSchema, context, args, isBackConvert: boolean) {
229-
const converter = { callback: null, context, args };
230-
let callback = isBackConvert ? converterSchema.toModel : converterSchema.toView;
231-
if (callback == null) {
232-
callback = Function.prototype;
236+
function getConverterCallback(value, isBackConvert: boolean) {
237+
let callback = null;
238+
if (isNullOrUndefined(value)) {
239+
callback = value;
240+
} else if (isFunction(value)) {
241+
callback = isBackConvert ? Function.prototype : value;
242+
} else if (isObject(value) && (isFunction(value.toModel) || isFunction(value.toView))) {
243+
callback = (isBackConvert ? value.toModel : value.toView) || Function.prototype;
244+
} else {
245+
callback = value;
233246
}
234-
converter.callback = callback;
235-
return converter;
247+
return callback;
236248
}
237249

238250
export function parseExpression(expressionText: string): ASTExpression {

0 commit comments

Comments
 (0)