mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-15 02:54:11 +08:00
feat: improved converter and function call parsing mechanism for XML expressions (#9805)
This commit is contained in:

committed by
GitHub

parent
b1640b0b3a
commit
c5856c6dae
@ -1,5 +1,5 @@
|
|||||||
// regex that contains all symbols applicable for expression used to AI detect an expression.
|
// regex that contains all symbols applicable for expression used to AI detect an expression.
|
||||||
const expressionSymbolsRegex = /[\+\-\*\/%\?:<>=!\|&\(\)^~]|\$\{.+\}/;
|
const expressionSymbolsRegex = /[\+\-\*\/%\?:<>=!\|&\(\)^~]|^`.*\$\{.+\}.*`$/;
|
||||||
|
|
||||||
export namespace bindingConstants {
|
export namespace bindingConstants {
|
||||||
export const sourceProperty = 'sourceProperty';
|
export const sourceProperty = 'sourceProperty';
|
||||||
|
@ -24,6 +24,7 @@ const binaryOperators = {
|
|||||||
'+': (l, r) => l + r,
|
'+': (l, r) => l + r,
|
||||||
'-': (l, r) => l - r,
|
'-': (l, r) => l - r,
|
||||||
'*': (l, r) => l * r,
|
'*': (l, r) => l * r,
|
||||||
|
'**': (l, r) => l ** r,
|
||||||
'/': (l, r) => l / r,
|
'/': (l, r) => l / r,
|
||||||
'%': (l, r) => l % r,
|
'%': (l, r) => l % r,
|
||||||
'<': (l, r) => l < r,
|
'<': (l, r) => l < r,
|
||||||
@ -63,54 +64,53 @@ const expressionParsers = {
|
|||||||
|
|
||||||
const left = convertExpressionToValue(expression.left, model, isBackConvert, changedModel);
|
const left = convertExpressionToValue(expression.left, model, isBackConvert, changedModel);
|
||||||
|
|
||||||
if (expression.operator == '|' && expression.right.type == 'CallExpression') {
|
let converterExpression = expression.right;
|
||||||
expression.right.requiresConverter = true;
|
if (expression.operator == '|') {
|
||||||
|
if (converterExpression.type == 'ChainExpression') {
|
||||||
|
converterExpression = converterExpression.expression;
|
||||||
|
}
|
||||||
|
if (converterExpression.type == 'CallExpression') {
|
||||||
|
!converterExpression.arguments.includes(expression.left) && converterExpression.arguments.unshift(expression.left);
|
||||||
|
expression.right.nsIsCallable = true;
|
||||||
|
converterExpression = converterExpression.callee;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (converterExpression.type) {
|
||||||
|
case 'Identifier':
|
||||||
|
case 'MemberExpression':
|
||||||
|
case 'NewExpression':
|
||||||
|
converterExpression.nsRequiresConverter = true;
|
||||||
|
converterExpression.nsIsPendingCall = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Invalid converter syntax');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const right = convertExpressionToValue(expression.right, model, isBackConvert, changedModel);
|
const right = convertExpressionToValue(expression.right, model, isBackConvert, changedModel);
|
||||||
|
|
||||||
if (expression.operator == '|') {
|
if (expression.operator == '|') {
|
||||||
if (expression.right.requiresConverter && right != null) {
|
if (converterExpression.nsRequiresConverter) {
|
||||||
right.args.unshift(left);
|
return expression.right.nsIsCallable ? right : right?.(left);
|
||||||
return right.callback.apply(right.context, right.args);
|
|
||||||
}
|
}
|
||||||
throw new Error('Invalid converter after ' + expression.operator + ' operator');
|
throw new Error('Invalid converter syntax');
|
||||||
}
|
}
|
||||||
return binaryOperators[expression.operator](left, right);
|
return binaryOperators[expression.operator](left, right);
|
||||||
},
|
},
|
||||||
'CallExpression': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
'CallExpression': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
||||||
expression.callee.requiresObjectAndProperty = true;
|
expression.callee.nsIsPendingCall = true;
|
||||||
|
const callback = convertExpressionToValue(expression.callee, model, isBackConvert, changedModel);
|
||||||
|
|
||||||
const { object, property } = convertExpressionToValue(expression.callee, model, isBackConvert, changedModel);
|
if (!expression.optional && isNullOrUndefined(callback)) {
|
||||||
|
|
||||||
let callback;
|
|
||||||
if (object == FORCED_CHAIN_VALUE) {
|
|
||||||
callback = undefined;
|
|
||||||
} else {
|
|
||||||
callback = expression.callee.optional ? object?.[property] : object[property];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((!expression.optional || expression.requiresConverter) && isNullOrUndefined(callback)) {
|
|
||||||
throw new Error('Cannot perform a call using a null or undefined property');
|
throw new Error('Cannot perform a call using a null or undefined property');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expression.requiresConverter) {
|
|
||||||
if (isFunction(callback)) {
|
|
||||||
callback = { toView: callback };
|
|
||||||
} else if (!isObject(callback) || !isFunction(callback.toModel) && !isFunction(callback.toView)) {
|
|
||||||
throw new Error('Invalid converter call');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedArgs = [];
|
const parsedArgs = [];
|
||||||
for (let argument of expression.arguments) {
|
for (let argument of expression.arguments) {
|
||||||
let value = convertExpressionToValue(argument, model, isBackConvert, changedModel);
|
let value = convertExpressionToValue(argument, model, isBackConvert, changedModel);
|
||||||
argument.type == 'SpreadElement' ? parsedArgs.push(...value) : parsedArgs.push(value);
|
argument.type == 'SpreadElement' ? parsedArgs.push(...value) : parsedArgs.push(value);
|
||||||
}
|
}
|
||||||
|
return expression.optional ? callback?.(...parsedArgs) : callback(...parsedArgs);
|
||||||
if (expression.requiresConverter) {
|
|
||||||
return getConverter(callback, object, parsedArgs, isBackConvert);
|
|
||||||
}
|
|
||||||
return expression.optional ? callback?.apply(object, parsedArgs) : callback.apply(object, parsedArgs);
|
|
||||||
},
|
},
|
||||||
'ChainExpression': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
'ChainExpression': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
||||||
return convertExpressionToValue(expression.expression, model, isBackConvert, changedModel);
|
return convertExpressionToValue(expression.expression, model, isBackConvert, changedModel);
|
||||||
@ -121,10 +121,11 @@ const expressionParsers = {
|
|||||||
},
|
},
|
||||||
'Identifier': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
'Identifier': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
||||||
const context = getContext(expression.name, model, changedModel);
|
const context = getContext(expression.name, model, changedModel);
|
||||||
if (expression.requiresObjectAndProperty) {
|
let value = context[expression.name];
|
||||||
return { object: context, property: expression.name };
|
if (expression.nsRequiresConverter) {
|
||||||
}
|
value = getConverterCallback(value, isBackConvert);
|
||||||
return context[expression.name];
|
}
|
||||||
|
return expression.nsIsPendingCall && typeof value === 'function' ? value.bind(context) : value;
|
||||||
},
|
},
|
||||||
'Literal': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
'Literal': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
||||||
return expression.regex != null ? new RegExp(expression.regex.pattern, expression.regex.flags) : expression.value;
|
return expression.regex != null ? new RegExp(expression.regex.pattern, expression.regex.flags) : expression.value;
|
||||||
@ -138,34 +139,34 @@ const expressionParsers = {
|
|||||||
},
|
},
|
||||||
'MemberExpression': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
'MemberExpression': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
||||||
if (expression.object.type == 'MemberExpression') {
|
if (expression.object.type == 'MemberExpression') {
|
||||||
expression.object.isChained = true;
|
expression.object.nsIsChained = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const object = convertExpressionToValue(expression.object, model, isBackConvert, changedModel);
|
const object = convertExpressionToValue(expression.object, model, isBackConvert, changedModel);
|
||||||
const property = expression.computed ? convertExpressionToValue(expression.property, model, isBackConvert, changedModel) : expression.property?.name;
|
const property = expression.computed ? convertExpressionToValue(expression.property, model, isBackConvert, changedModel) : expression.property?.name;
|
||||||
const propertyInfo = { object, property };
|
|
||||||
|
|
||||||
if (expression.requiresObjectAndProperty) {
|
|
||||||
return propertyInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If first member is undefined, make sure that no error is thrown later but return undefined instead.
|
* If an expression parent property is null or undefined, apply null-safety.
|
||||||
* This behaviour is kept in order to cope with components whose binding context takes a bit long to load.
|
* This behaviour also helps cope with components whose binding context takes a bit longer to load.
|
||||||
* Old parser would return undefined for an expression like 'property1.property2.property3'
|
* Old parser would be null-safe for properties and sub-properties
|
||||||
* even if expression as a whole consisted of undefined properties.
|
* even if expression as a whole consisted of undefined ones.
|
||||||
* The new one will keep the same principle only if first member is undefined for safety reasons.
|
* The new parser will keep the same principle only if parent property is null or undefined, resulting in better control over code and errors.
|
||||||
* It meddles with members specifically, so that it will not affect expression result as a whole.
|
* It meddles with members specifically, so that it will not affect expression result as a whole.
|
||||||
* For example, an 'isLoading || isBusy' expression will be validated as 'undefined || undefined'
|
* For example, an 'isLoading || isBusy' expression will be validated as 'undefined || undefined'
|
||||||
* if context is not ready.
|
* if context is not ready.
|
||||||
*/
|
*/
|
||||||
if (object === undefined && expression.object.type == 'Identifier') {
|
if (object == null && expression.object.type == 'Identifier') {
|
||||||
return expression.isChained ? FORCED_CHAIN_VALUE : object;
|
return expression.nsIsChained ? FORCED_CHAIN_VALUE : undefined;
|
||||||
}
|
}
|
||||||
if (object == FORCED_CHAIN_VALUE) {
|
if (object == FORCED_CHAIN_VALUE) {
|
||||||
return expression.isChained ? object : undefined;
|
return expression.nsIsChained ? object : undefined;
|
||||||
}
|
}
|
||||||
return expression.optional ? object?.[property] : object[property];
|
|
||||||
|
let value = expression.optional ? object?.[property] : object[property];
|
||||||
|
if (expression.nsRequiresConverter) {
|
||||||
|
value = getConverterCallback(value, isBackConvert);
|
||||||
|
}
|
||||||
|
return expression.nsIsPendingCall && typeof value === 'function' ? value.bind(object) : value;
|
||||||
},
|
},
|
||||||
'NewExpression': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
'NewExpression': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
||||||
const callback = convertExpressionToValue(expression.callee, model, isBackConvert, changedModel);
|
const callback = convertExpressionToValue(expression.callee, model, isBackConvert, changedModel);
|
||||||
@ -174,7 +175,12 @@ const expressionParsers = {
|
|||||||
let value = convertExpressionToValue(argument, model, isBackConvert, changedModel);
|
let value = convertExpressionToValue(argument, model, isBackConvert, changedModel);
|
||||||
argument.type == 'SpreadElement' ? parsedArgs.push(...value) : parsedArgs.push(value);
|
argument.type == 'SpreadElement' ? parsedArgs.push(...value) : parsedArgs.push(value);
|
||||||
}
|
}
|
||||||
return new callback(...parsedArgs);
|
|
||||||
|
let value = new callback(...parsedArgs);
|
||||||
|
if (expression.nsRequiresConverter) {
|
||||||
|
value = getConverterCallback(value, isBackConvert);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
},
|
},
|
||||||
'ObjectExpression': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
'ObjectExpression': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
||||||
const parsedObject = {};
|
const parsedObject = {};
|
||||||
@ -198,12 +204,14 @@ const expressionParsers = {
|
|||||||
},
|
},
|
||||||
'TemplateLiteral': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
'TemplateLiteral': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
||||||
let parsedText = '';
|
let parsedText = '';
|
||||||
for (let q of expression.quasis) {
|
const length = expression.quasis.length;
|
||||||
parsedText += convertExpressionToValue(q, model, isBackConvert, changedModel);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let ex of expression.expressions) {
|
for (let i = 0; i < length; i++) {
|
||||||
parsedText += convertExpressionToValue(ex, model, isBackConvert, changedModel);
|
let q = expression.quasis[i];
|
||||||
|
parsedText += convertExpressionToValue(q, model, isBackConvert, changedModel);
|
||||||
|
if (!q.tail) {
|
||||||
|
parsedText += convertExpressionToValue(expression.expressions[i], model, isBackConvert, changedModel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return parsedText;
|
return parsedText;
|
||||||
},
|
},
|
||||||
@ -225,14 +233,18 @@ function getContext(key, model, changedModel) {
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConverter(converterSchema, context, args, isBackConvert: boolean) {
|
function getConverterCallback(value, isBackConvert: boolean) {
|
||||||
const converter = { callback: null, context, args };
|
let callback = null;
|
||||||
let callback = isBackConvert ? converterSchema.toModel : converterSchema.toView;
|
if (isNullOrUndefined(value)) {
|
||||||
if (callback == null) {
|
callback = value;
|
||||||
callback = Function.prototype;
|
} else if (isFunction(value)) {
|
||||||
|
callback = isBackConvert ? Function.prototype : value;
|
||||||
|
} else if (isObject(value) && (isFunction(value.toModel) || isFunction(value.toView))) {
|
||||||
|
callback = (isBackConvert ? value.toModel : value.toView) || Function.prototype;
|
||||||
|
} else {
|
||||||
|
callback = value;
|
||||||
}
|
}
|
||||||
converter.callback = callback;
|
return callback;
|
||||||
return converter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseExpression(expressionText: string): ASTExpression {
|
export function parseExpression(expressionText: string): ASTExpression {
|
||||||
|
Reference in New Issue
Block a user