@nativescript/core
Version:
A JavaScript library providing an easy to use api for interacting with iOS and Android platform APIs.
255 lines • 12 kB
JavaScript
import { parse } from 'acorn';
import { isFunction, isNullOrUndefined, isObject } from '../../../utils/types';
const expressionsCache = {};
const FORCED_CHAIN_VALUE = Symbol('forcedChain');
// prettier-ignore
const unaryOperators = {
'+': (v) => +v,
'-': (v) => -v,
'!': (v) => !v,
'void': (v) => void v,
'typeof': (v) => typeof v
};
// prettier-ignore
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,
'!==': (l, r) => l !== r,
'|': (l, r) => l | r,
'in': (l, r) => l in r,
'instanceof': (l, r) => l instanceof r
};
// prettier-ignore
const logicalOperators = {
'&&': (l, r) => l && r(),
'||': (l, r) => l || r(),
'??': (l, r) => l ?? r()
};
// prettier-ignore
const expressionParsers = {
'ArrayExpression': (expression, model, isBackConvert, changedModel) => {
const parsed = [];
for (const element of expression.elements) {
const value = convertExpressionToValue(element, model, isBackConvert, changedModel);
element.type == 'SpreadElement' ? parsed.push(...value) : parsed.push(value);
}
return parsed;
},
'BinaryExpression': (expression, model, isBackConvert, changedModel) => {
if (binaryOperators[expression.operator] == null) {
throw new Error('Disallowed binary operator: ' + expression.operator);
}
const left = convertExpressionToValue(expression.left, model, isBackConvert, changedModel);
let converterExpression = expression.right;
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);
if (expression.operator == '|') {
if (converterExpression.nsRequiresConverter) {
if (expression.right.nsIsCallable) {
return right;
}
if (isFunction(right)) {
return right(left);
}
if (isNullOrUndefined(right)) {
throw new Error('Cannot perform a call using a null or undefined property');
}
}
throw new Error('Invalid converter syntax');
}
return binaryOperators[expression.operator](left, right);
},
'CallExpression': (expression, model, isBackConvert, changedModel) => {
expression.callee.nsIsPendingCall = true;
const callback = convertExpressionToValue(expression.callee, model, isBackConvert, changedModel);
if (!expression.optional && isNullOrUndefined(callback)) {
throw new Error('Cannot perform a call using a null or undefined property');
}
const parsedArgs = [];
for (const argument of expression.arguments) {
const value = convertExpressionToValue(argument, model, isBackConvert, changedModel);
argument.type == 'SpreadElement' ? parsedArgs.push(...value) : parsedArgs.push(value);
}
return expression.optional ? callback?.(...parsedArgs) : callback(...parsedArgs);
},
'ChainExpression': (expression, model, isBackConvert, changedModel) => {
return convertExpressionToValue(expression.expression, model, isBackConvert, changedModel);
},
'ConditionalExpression': (expression, model, isBackConvert, changedModel) => {
const test = convertExpressionToValue(expression.test, model, isBackConvert, changedModel);
return convertExpressionToValue(expression[test ? 'consequent' : 'alternate'], model, isBackConvert, changedModel);
},
'Identifier': (expression, model, isBackConvert, changedModel) => {
const context = getContext(expression.name, model, changedModel);
let value = context[expression.name];
if (expression.nsRequiresConverter) {
value = getConverterCallback(value, isBackConvert);
}
return expression.nsIsPendingCall && typeof value === 'function' ? value.bind(context) : value;
},
'Literal': (expression, model, isBackConvert, changedModel) => {
return expression.regex != null ? new RegExp(expression.regex.pattern, expression.regex.flags) : expression.value;
},
'LogicalExpression': (expression, model, isBackConvert, changedModel) => {
if (logicalOperators[expression.operator] == null) {
throw new Error('Disallowed logical operator: ' + expression.operator);
}
const left = convertExpressionToValue(expression.left, model, isBackConvert, changedModel);
return logicalOperators[expression.operator](left, () => convertExpressionToValue(expression.right, model, isBackConvert, changedModel));
},
'MemberExpression': (expression, model, isBackConvert, changedModel) => {
if (expression.object.type == 'MemberExpression') {
expression.object.nsIsChained = true;
}
const object = convertExpressionToValue(expression.object, model, isBackConvert, changedModel);
const property = expression.computed ? convertExpressionToValue(expression.property, model, isBackConvert, changedModel) : expression.property?.name;
/**
* If an expression parent property is null or undefined, apply null-safety.
* This behaviour also helps cope with components whose binding context takes a bit longer to load.
* Old parser would be null-safe for properties and sub-properties
* even if expression as a whole consisted of undefined ones.
* 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.
* For example, an 'isLoading || isBusy' expression will be validated as 'undefined || undefined'
* if context is not ready.
*/
if (object == null && expression.object.type == 'Identifier') {
return expression.nsIsChained ? FORCED_CHAIN_VALUE : undefined;
}
if (object == FORCED_CHAIN_VALUE) {
return expression.nsIsChained ? object : undefined;
}
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, model, isBackConvert, changedModel) => {
const callback = convertExpressionToValue(expression.callee, model, isBackConvert, changedModel);
const parsedArgs = [];
for (const argument of expression.arguments) {
const value = convertExpressionToValue(argument, model, isBackConvert, changedModel);
argument.type == 'SpreadElement' ? parsedArgs.push(...value) : parsedArgs.push(value);
}
let value = new callback(...parsedArgs);
if (expression.nsRequiresConverter) {
value = getConverterCallback(value, isBackConvert);
}
return value;
},
'ObjectExpression': (expression, model, isBackConvert, changedModel) => {
const parsedObject = {};
for (const property of expression.properties) {
const value = convertExpressionToValue(property, model, isBackConvert, changedModel);
Object.assign(parsedObject, value);
}
return parsedObject;
},
'Property': (expression, model, isBackConvert, changedModel) => {
const key = expression.computed ? convertExpressionToValue(expression.key, model, isBackConvert, changedModel) : expression.key?.name;
const value = convertExpressionToValue(expression.value, model, isBackConvert, changedModel);
return { [key]: value };
},
'SpreadElement': (expression, model, isBackConvert, changedModel) => {
const argument = convertExpressionToValue(expression.argument, model, isBackConvert, changedModel);
return argument;
},
'TemplateElement': (expression, model, isBackConvert, changedModel) => {
return expression.value.cooked;
},
'TemplateLiteral': (expression, model, isBackConvert, changedModel) => {
let parsedText = '';
const length = expression.quasis.length;
for (let i = 0; i < length; i++) {
const q = expression.quasis[i];
parsedText += convertExpressionToValue(q, model, isBackConvert, changedModel);
if (!q.tail) {
parsedText += convertExpressionToValue(expression.expressions[i], model, isBackConvert, changedModel);
}
}
return parsedText;
},
'UnaryExpression': (expression, model, isBackConvert, changedModel) => {
if (unaryOperators[expression.operator] == null) {
throw Error('Disallowed unary operator: ' + expression.operator);
}
const argument = convertExpressionToValue(expression.argument, model, isBackConvert, changedModel);
return unaryOperators[expression.operator](argument);
}
};
function getContext(key, model, changedModel) {
let context = key in changedModel ? changedModel : model;
if (!(key in context)) {
context = global;
}
return context;
}
function getConverterCallback(value, isBackConvert) {
let callback = null;
if (isNullOrUndefined(value)) {
callback = value;
}
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;
}
return callback;
}
export function parseExpression(expressionText) {
let expression = expressionsCache[expressionText];
if (expression == null) {
const program = parse(expressionText, { ecmaVersion: 2020 });
const statements = program.body;
for (const statement of statements) {
if (statement.type == 'ExpressionStatement') {
expression = statement.expression;
break;
}
}
expressionsCache[expressionText] = expression;
}
return expression;
}
export function convertExpressionToValue(expression, model, isBackConvert, changedModel) {
if (!(expression.type in expressionParsers)) {
throw Error('Invalid expression syntax');
}
return expressionParsers[expression.type](expression, model, isBackConvert, changedModel);
}
//# sourceMappingURL=bindable-expressions.js.map