@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
937 lines (879 loc) • 30.5 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2026 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
sap.ui.define([
"sap/base/Log",
"sap/base/strings/escapeRegExp",
"sap/base/util/deepEqual",
"sap/base/util/JSTokenizer",
"sap/ui/performance/Measurement"
], function (Log, escapeRegExp, deepEqual, JSTokenizer, Measurement) {
"use strict";
//SAP's Independent Implementation of "Top Down Operator Precedence" by Vaughan R. Pratt,
// see http://portal.acm.org/citation.cfm?id=512931
//Inspired by "TDOP" of Douglas Crockford which is also an implementation of Pratt's article
// see https://github.com/douglascrockford/TDOP
//License granted by Douglas Crockford to SAP, Apache License 2.0
// (http://www.apache.org/licenses/LICENSE-2.0)
//
//led = "left denotation"
//lbp = "left binding power", for values see
//https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence
//nud = "null denotation"
//rbp = "right binding power"
var fnUndefined = CONSTANT.bind(null, undefined),
mDefaultGlobals = {
"Array": Array,
"Boolean": Boolean,
"Date": Date,
"encodeURIComponent": encodeURIComponent,
"Infinity": Infinity,
"isFinite": isFinite,
"isNaN": isNaN,
"JSON": JSON,
"Math": Math,
"NaN": NaN,
"Number": Number,
"Object": Object,
"odata": {
"collection": function (aElements) {
return aElements.filter(function (vElement) {
return vElement !== undefined;
});
},
"compare": function () {
var oODataUtils = sap.ui.require("sap/ui/model/odata/v4/ODataUtils");
/** @deprecated As of version 1.120.0 */
if (!oODataUtils) {
oODataUtils = sap.ui.requireSync("sap/ui/model/odata/v4/ODataUtils");
}
if (!oODataUtils) {
throw new TypeError("Expression uses 'odata.compare' which requires to"
+ " import 'sap/ui/model/odata/v4/ODataUtils' in advance");
}
return oODataUtils.compare.apply(oODataUtils, arguments);
},
"fillUriTemplate": function (sExpression, mData) {
let URITemplate = sap.ui.require("sap/ui/thirdparty/URITemplate");
/** @deprecated As of version 1.120.0 */
if (!URITemplate) {
// probing is not required since the presence of URI.expand is the indicator
// that URITemplate has been loaded already
URITemplate = sap.ui.requireSync("sap/ui/thirdparty/URITemplate");
}
if (!URITemplate) {
throw new TypeError("Expression uses 'odata.fillUriTemplate' which requires"
+ " to import 'sap/ui/thirdparty/URITemplate' in advance");
}
return new URITemplate(sExpression.trim()).expand(mData);
},
"uriEncode": function () {
var oODataUtils = sap.ui.require("sap/ui/model/odata/ODataUtils");
/** @deprecated As of version 1.120.0 */
if (!oODataUtils) {
oODataUtils = sap.ui.requireSync("sap/ui/model/odata/ODataUtils");
}
if (!oODataUtils) {
throw new TypeError("Expression uses 'odata.uriEncode' which requires to"
+ " import 'sap/ui/model/odata/ODataUtils' in advance");
}
return oODataUtils.formatValue.apply(oODataUtils, arguments);
}
},
"parseFloat": parseFloat,
"parseInt": parseInt,
"RegExp": RegExp,
"String": String,
"undefined": undefined
},
rDigit = /\d/,
sExpressionParser = "sap.ui.base.ExpressionParser",
rIdentifier = /[a-z_$][a-z0-9_$]*/i,
rIdentifierStart = /[a-z_$]/i,
aPerformanceCategories = [sExpressionParser],
sPerformanceParse = sExpressionParser + "#parse",
mSymbols = { //symbol table
"BINDING": {
led: unexpected, // Note: cannot happen due to lbp: 0
nud: function (oToken, oParser) {
return BINDING.bind(null, oToken.value);
}
},
"ERROR": {
lbp: Infinity,
led: function (oToken, oParser, fnLeft) {
error(oToken.value.message, oToken.value.text, oToken.value.at);
},
nud: function (oToken, oParser) {
error(oToken.value.message, oToken.value.text, oToken.value.at);
}
},
"IDENTIFIER": {
led: unexpected, // Note: cannot happen due to lbp: 0
nud: function (oToken, oParser) {
if (!(oToken.value in oParser.globals)) {
Log.warning("Unsupported global identifier '" + oToken.value
+ "' in expression parser input '" + oParser.input + "'",
undefined,
sExpressionParser);
}
return CONSTANT.bind(null, oParser.globals[oToken.value]);
}
},
"CONSTANT": {
led: unexpected, // Note: cannot happen due to lbp: 0
nud: function (oToken, oParser) {
return CONSTANT.bind(null, oToken.value);
}
},
".": {
lbp: 18,
led: function (oToken, oParser, fnLeft) {
return DOT.bind(null, fnLeft, oParser.advance("IDENTIFIER").value);
},
nud: unexpected
},
"(": {
lbp: 17,
led: function (oToken, oParser, fnLeft) {
var aArguments = [],
bFirst = true;
while (oParser.current().id !== ")") {
if (bFirst) {
bFirst = false;
} else {
oParser.advance(","); //consume "," from predecessor argument
}
aArguments.push(oParser.expression(0));
}
oParser.advance(")");
return FUNCTION_CALL.bind(null, fnLeft, aArguments);
},
nud: function (oToken, oParser) {
var fnValue = oParser.expression(0);
oParser.advance(")");
return fnValue;
}
},
"[": {
lbp: 18,
led: function (oToken, oParser, fnLeft) {
var fnName = oParser.expression(0);
oParser.advance("]");
return PROPERTY_ACCESS.bind(null, fnLeft, fnName);
},
nud: function (oToken, oParser) {
var aElements = [],
bFirst = true;
while (oParser.current().id !== "]") {
if (bFirst) {
bFirst = false;
} else {
oParser.advance(","); //consume "," from predecessor element
}
aElements.push(
oParser.current().id === "," ? fnUndefined : oParser.expression(0));
}
oParser.advance("]");
return ARRAY.bind(null, aElements);
}
},
"!": {
lbp: 15,
led: unexpected,
nud: function (oToken, oParser) {
return UNARY.bind(null, oParser.expression(this.lbp), function (x) {
return !x;
});
}
},
"typeof": {
lbp: 15,
led: unexpected,
nud: function (oToken, oParser) {
return UNARY.bind(null, oParser.expression(this.lbp), function (x) {
return typeof x;
});
}
},
"?": {
lbp: 4,
led: function (oToken, oParser, fnLeft) {
var fnElse, fnThen;
fnThen = oParser.expression(this.lbp - 1);
oParser.advance(":");
fnElse = oParser.expression(this.lbp - 1);
return CONDITIONAL.bind(null, fnLeft, fnThen, fnElse);
},
nud: unexpected
},
")": {
led: unexpected,
nud: unexpected
},
"]": {
led: unexpected,
nud: unexpected
},
"{": {
led: unexpected,
nud: function (oToken, oParser) {
var bFirst = true,
sKey,
mMap = {},
fnValue;
while (oParser.current().id !== "}") {
if (bFirst) {
bFirst = false;
} else {
oParser.advance(",");
}
if (oParser.current() && oParser.current().id === "CONSTANT"
&& typeof oParser.current().value === "string") {
sKey = oParser.advance().value;
} else {
sKey = oParser.advance("IDENTIFIER").value;
}
oParser.advance(":");
fnValue = oParser.expression(0);
mMap[sKey] = fnValue;
}
oParser.advance("}");
return MAP.bind(null, mMap);
}
},
"}": {
lbp: -1, // Note: also terminates end of our input!
led: unexpected,
nud: unexpected
},
",": {
led: unexpected,
nud: unexpected
},
":": {
led: unexpected,
nud: unexpected
}
},
//Fix length tokens. A token being a prefix of another must come last, e.g. ! after !==
aTokens = ["===", "!==", "!", "||", "&&", ".", "(", ")", "{", "}", ":", ",", "?", "*",
"/", "%", "+", "-", "<=", "<", ">=", ">", "[", "]"],
rTokens;
aTokens.forEach(function (sToken, i) {
// Note: this function is executed at load time only!
aTokens[i] = escapeRegExp(sToken);
});
rTokens = new RegExp(aTokens.join("|"), "g");
addInfix("*", 14, function (x, y) {
return x * y;
});
addInfix("/", 14, function (x, y) {
return x / y;
});
addInfix("%", 14, function (x, y) {
return x % y;
});
addInfix("+", 13, function (x, y) {
return x + y;
}).nud = function (oToken, oParser) {
return UNARY.bind(null, oParser.expression(this.lbp), function (x) {
return +x;
});
};
addInfix("-", 13, function (x, y) {
return x - y;
}).nud = function (oToken, oParser) {
return UNARY.bind(null, oParser.expression(this.lbp), function (x) {
return -x;
});
};
addInfix("<=", 11, function (x, y) {
return x <= y;
});
addInfix("<", 11, function (x, y) {
return x < y;
});
addInfix(">=", 11, function (x, y) {
return x >= y;
});
addInfix(">", 11, function (x, y) {
return x > y;
});
addInfix("in", 11, function (x, y) {
return x in y;
});
addInfix("===", 10, function (x, y) {
return x === y;
});
addInfix("!==", 10, function (x, y) {
return x !== y;
});
addInfix("&&", 7, function (x, fnY) {
return x && fnY();
}, true);
addInfix("||", 6, function (x, fnY) {
return x || fnY();
}, true);
//Formatter functions to evaluate symbols like literals or operators in the expression grammar
/**
* Formatter function for an array literal.
* @param {function[]} aElements - array of formatter functions for the array elements
* @param {any[]} aParts - the array of binding values
* @return {any[]} - the resulting array value
*/
function ARRAY(aElements, aParts) {
return aElements.map(function (fnElement) {
return fnElement(aParts);
});
}
/**
* Formatter function for an embedded binding.
* @param {int} i - the index of the binding as it appears when reading the
* expression from the left
* @param {any[]} aParts - the array of binding values
* @returns {any} the binding value
*/
function BINDING(i, aParts) {
return clean(aParts[i]);
}
/**
* Formatter function for executing the conditional operator with the given condition, "then"
* and "else" clause.
* @param {function} fnCondition - formatter function for the condition
* @param {function} fnThen - formatter function for the "then" clause
* @param {function} fnElse - formatter function for the "else" clause
* @param {any[]} aParts - the array of binding values
* @return {any} - the value of the "then" or "else" clause, depending on the value of the
* condition
*/
function CONDITIONAL(fnCondition, fnThen, fnElse, aParts) {
return fnCondition(aParts) ? fnThen(aParts) : fnElse(aParts);
}
/**
* Formatter function for any constant value such as a literal or identifier.
* @param {any} v - any value
* @returns {any} the given value
*/
function CONSTANT(v) {
return v;
}
/**
* Formatter function for member access via the dot operator.
* @param {function} fnLeft - formatter function for the left operand
* @param {string} sIdentifier - the identifier on the dot's right side
* @param {any[]} aParts - the array of binding values
* @param {object} [oReference]
* optional side channel to return the base value (left operand) of the reference
* @return {any} - the left operand's member with the name
*/
function DOT(fnLeft, sIdentifier, aParts, oReference) {
var oParent = fnLeft(aParts),
vChild = oParent[sIdentifier];
if (oReference) {
oReference.base = oParent;
}
return clean(vChild);
}
/**
* Formatter function for a call to the function returned by fnLeft.
* @param {function} fnLeft - formatter function for the left operand: the function to call
* @param {function[]} aArguments - array of formatter functions for the arguments
* @param {any[]} aParts - the array of binding values
* @return {any} - the return value of the function applied to the arguments
*/
function FUNCTION_CALL(fnLeft, aArguments, aParts) {
var oReference = {};
// evaluate function expression and call it
return clean(fnLeft(aParts, oReference).apply(oReference.base,
aArguments.map(function (fnArgument) {
return fnArgument(aParts); // evaluate argument
})));
}
/**
* Formatter function for an infix operator.
*
* @param {function} fnLeft - formatter function for the left operand
* @param {function} fnRight - formatter function for the right operand
* @param {function} fnOperator
* function taking two arguments which evaluates the infix operator
* @param {boolean} bLazy - whether the right operand is e
* @param {any[]} aParts - the array of binding values
* @return {any} - the result of the operator function applied to the two operands
*/
function INFIX(fnLeft, fnRight, fnOperator, bLazy, aParts) {
return fnOperator(fnLeft(aParts),
bLazy ? fnRight.bind(null, aParts) : fnRight(aParts));
}
/**
* Formatter function for an object literal.
* @param {object} mMap - map from key to formatter functions for the values
* @param {any[]} aParts - the array of binding values
* @return {object} - the resulting map
*/
function MAP(mMap, aParts) {
var sKey, mResult = {};
for (sKey in mMap) {
mResult[sKey] = mMap[sKey](aParts); // evaluate value
}
return mResult;
}
/**
* Formatter function for a property access.
* @param {function} fnLeft - formatter function for the left operand: the array or object to
* access
* @param {function} fnName - formatter function for the property name
* @param {any[]} aParts - the array of binding values
* @param {object} [oReference]
* optional side channel to return the base value (left operand) of the reference
* @return {any} - the array element or object property
*/
function PROPERTY_ACCESS(fnLeft, fnName, aParts, oReference) {
var oParent = fnLeft(aParts),
sIdentifier = fnName(aParts), // BEWARE: evaluate propertyNameValue AFTER baseValue!
vChild = oParent[sIdentifier];
if (oReference) {
oReference.base = oParent;
}
return clean(vChild);
}
/**
* Formatter function for a unary operator.
*
* @param {function} fnRight - formatter function for the operand
* @param {function} fnOperator
* function to evaluate the unary operator taking one argument
* @param {any[]} aParts - the array of binding values
* @return {any} - the result of the operator function applied to the operand
*/
function UNARY(fnRight, fnOperator, aParts) {
return fnOperator(fnRight(aParts));
}
/**
* Adds the infix operator with the given id, binding power and formatter function to the
* symbol table.
* @param {string} sId - the id of the infix operator
* @param {int} iBindingPower - the binding power = precedence of the infix operator
* @param {function} fnOperator - the function to evaluate the operator
* @param {boolean} [bLazy=false] - whether the right operand is lazily evaluated
* @return {object} the newly created symbol for the infix operator
*/
function addInfix(sId, iBindingPower, fnOperator, bLazy) {
// Note: this function is executed at load time only!
mSymbols[sId] = {
lbp: iBindingPower,
led: function (oToken, oParser, fnLeft) {
//lazy evaluation is right associative: performance optimization for guard and
//default operator, e.g. true || A || B || C does not execute the || for B and C
var rbp = bLazy ? this.lbp - 1 : this.lbp;
return INFIX.bind(null, fnLeft, oParser.expression(rbp),
fnOperator, bLazy);
},
nud: unexpected
};
return mSymbols[sId];
}
/**
* Cleans the given <code>vValue</code>.
*
* @param {any} vValue - the value to be cleaned
* @returns {any} the cleaned value
*/
function clean(vValue) {
return vValue === Function ? undefined : vValue;
}
/**
* Throws a SyntaxError with the given <code>sMessage</code> as <code>message</code>, its
* <code>at</code> property set to <code>iAt</code> and its <code>text</code> property to
* <code>sInput</code>.
* In addition, logs a corresponding error message to the console with <code>sInput</code>
* as details.
*
* @param {string} sMessage - the error message
* @param {string} sInput - the input string
* @param {int} [iAt] - the index in the input string where the error occurred; the index
* starts counting at 1 to be consistent with positions provided in tokenizer error messages.
*/
function error(sMessage, sInput, iAt) {
var oError = new SyntaxError(sMessage);
oError.at = iAt;
oError.text = sInput;
if (iAt !== undefined) {
sMessage += " at position " + iAt;
}
Log.error(sMessage, sInput, sExpressionParser);
throw oError;
}
/**
* Throws and logs an error for the unexpected token oToken.
* @param {object} oToken - the unexpected token
*/
function unexpected(oToken) {
// Note: position for error starts counting at 1
error("Unexpected " + oToken.id, oToken.input, oToken.start + 1);
}
/**
* Computes the tokens according to the expression grammar in sInput starting at iStart and
* uses fnResolveBinding to resolve bindings embedded in the expression.
* @param {function} fnResolveBinding - the function to resolve embedded bindings
* @param {string} sInput - the string to be parsed
* @param {int} [iStart=0] - the index to start parsing
* @returns {object} Tokenization result object with the following properties
* at: the index after the last character consumed by the tokenizer in the input string
* parts: array with parts corresponding to resolved embedded bindings
* tokens: the array of tokens where each token is a tuple of ID, optional value, and
* optional source text
*/
function tokenize(fnResolveBinding, sInput, iStart) {
var aParts = [], // the resulting parts (corresponds to aPrimitiveValueBindings)
aPrimitiveValueBindings = [], // the bindings with primitive values only
aTokens = [],
oTokenizer = new JSTokenizer();
/**
* Saves the binding as a part. Reuses an existing part if the binding is identical.
* @param {object} oBinding
* the binding to save
* @param {int} iStart
* the binding's start index in the input string
* @param {boolean} [bTargetTypeAny=false]
* whether the binding's "targetType" should default to "any" (recursively, for all parts)
* @returns {int}
* the index at which it has been saved/found in aParts
*/
function saveBindingAsPart(oBinding, iStart, bTargetTypeAny) {
var bHasNonPrimitiveValue = false,
sKey,
oPrimitiveValueBinding,
i;
/*
* Sets the target type of the given binding to the default "any", if applicable.
*
* @param {object} oBinding
* A binding
*/
function setTargetType(oBinding) {
if (bTargetTypeAny) {
if (oBinding.parts) {
oBinding.parts.forEach(setTargetType);
// Note: targetType not allowed here, see BindingParser.mergeParts
} else {
oBinding.targetType = oBinding.targetType || "any";
}
}
}
for (sKey in oBinding) {
if (sKey === "parameters") {
// parameters are not converted from name to object, but even a simple binding
// may have the implicit object parameter "scope"
continue;
}
switch (typeof oBinding[sKey]) {
case "boolean":
case "number":
case "string":
case "undefined":
break;
default:
// binding has at least one property of non-primitive value
bHasNonPrimitiveValue = true;
}
}
setTargetType(oBinding);
if (bHasNonPrimitiveValue) {
// the binding must be a complex binding; property "type" (and poss. others) are
// newly created objects and thus incomparable -> parse again to have the names
oPrimitiveValueBinding = JSTokenizer.parseJS(sInput, iStart).result;
setTargetType(oPrimitiveValueBinding);
} else {
// only primitive values; easily comparable
oPrimitiveValueBinding = oBinding;
}
for (i = 0; i < aParts.length; i += 1) {
// Note: order of top-level properties must not matter for equality!
if (deepEqual(aPrimitiveValueBindings[i], oPrimitiveValueBinding)) {
return i;
}
}
aPrimitiveValueBindings[i] = oPrimitiveValueBinding;
aParts[i] = oBinding;
return i;
}
/**
* Consumes the next token in the input string and pushes it to the array of tokens.
*
* @returns {boolean} whether a token is recognized
* @throws {Error|Object|SyntaxError}
* <code>fnResolveBinding</code> may throw <code>SyntaxError</code>;
* <code>oTokenizer.setIndex()</code> may throw <code>Error</code>;
* <code>oTokenizer</code> may also throw <code>{name: 'SyntaxError', ...}</code>
*/
function consumeToken() {
var ch, oBinding, iIndex, aMatches, oToken;
oTokenizer.white();
ch = oTokenizer.getCh();
iIndex = oTokenizer.getIndex();
if ((ch === "$" || ch === "%") && sInput[iIndex + 1] === "{") { //binding
oBinding = fnResolveBinding(sInput, iIndex + 1);
oToken = {
id: "BINDING",
value: saveBindingAsPart(oBinding.result, iIndex + 1, ch === "%")
};
oTokenizer.setIndex(oBinding.at); //go to first character after binding string
} else if (rIdentifierStart.test(ch)) {
aMatches = rIdentifier.exec(sInput.slice(iIndex));
switch (aMatches[0]) {
case "false":
case "null":
case "true":
oToken = {id: "CONSTANT", value: oTokenizer.word()};
break;
case "in":
case "typeof":
oToken = {id: aMatches[0]};
oTokenizer.setIndex(iIndex + aMatches[0].length);
break;
default:
oToken = {id: "IDENTIFIER", value: aMatches[0]};
oTokenizer.setIndex(iIndex + aMatches[0].length);
}
} else if (rDigit.test(ch)
|| ch === "." && rDigit.test(sInput[iIndex + 1])) {
oToken = {id: "CONSTANT", value: oTokenizer.number()};
} else if (ch === "'" || ch === '"') {
oToken = {id: "CONSTANT", value: oTokenizer.string()};
} else {
rTokens.lastIndex = iIndex;
aMatches = rTokens.exec(sInput);
if (!aMatches || aMatches.index !== iIndex) {
return false; // end of input or unrecognized character
}
oToken = {id: aMatches[0]};
oTokenizer.setIndex(iIndex + aMatches[0].length);
}
oToken.input = sInput;
oToken.start = iIndex;
oToken.end = oTokenizer.getIndex();
aTokens.push(oToken);
return true;
}
oTokenizer.init(sInput, iStart);
try {
/* eslint-disable no-empty */
while (consumeToken()) { /* deliberately empty */ }
/* eslint-enable no-empty */
} catch (e) {
// Note: new SyntaxError().name === "SyntaxError"
if (e.name === "SyntaxError") { // remember tokenizer error
aTokens.push({
id: "ERROR",
value: e
});
} else {
throw e;
}
}
return {
at: oTokenizer.getIndex(),
parts: aParts,
tokens: aTokens
};
}
/**
* Returns a function which wraps the given formatter function into a try/catch block.
* In case of an error it is caught, a warning containing the given original input is issued,
* and <code>undefined</code> is returned instead.
*
* @param {function} fnFormatter - any (formatter) function
* @param {string} sInput - the expression string (used when logging errors)
* @returns {function} - the wrapped function
*/
function tryCatch(fnFormatter, sInput) {
return function () {
try {
return fnFormatter.apply(this, arguments);
} catch (ex) {
Log.warning(String(ex), sInput, sExpressionParser);
}
};
}
/**
* Parses expression tokens to a result object as specified to be returned by
* {@link sap.ui.base.ExpressionParser#parse}.
* @param {object[]} aTokens - the array with the tokens
* @param {string} sInput - the expression string (used when logging errors)
* @param {object} mGlobals - the map of global variables
* @returns {object} the parse result with the following properties
* formatter: the formatter function to evaluate the expression which
* takes the parts corresponding to bindings embedded in the expression as
* parameters; undefined in case of an invalid expression
* at: the index of the first character after the expression in sInput, or
* <code>undefined</code> if all tokens have been consumed
*/
function parse(aTokens, sInput, mGlobals) {
var fnFormatter,
iNextToken = 0,
oParser = {
advance: advance,
current: current,
expression: expression,
globals: mGlobals,
input: sInput
},
oToken;
/**
* Returns the next token in the array of tokens and advances the index in this array.
* Throws an error if the next token's ID is not equal to the optional
* <code>sExpectedTokenId</code>.
* @param {string} [sExpectedTokenId] - the expected id of the next token
* @returns {object|undefined} - the next token or undefined if all tokens have been read
*/
function advance(sExpectedTokenId) {
var oToken = aTokens[iNextToken];
if (sExpectedTokenId) {
if (!oToken) {
error("Expected " + sExpectedTokenId + " but instead saw end of input",
sInput);
} else if (oToken.id !== sExpectedTokenId) {
error("Expected " + sExpectedTokenId + " but instead saw "
+ sInput.slice(oToken.start, oToken.end),
sInput,
oToken.start + 1);
}
}
iNextToken += 1;
return oToken;
}
/**
* Returns the next token in the array of tokens, but does not advance the index.
* @returns {object|undefined} - the next token or undefined if all tokens have been read
*/
function current() {
return aTokens[iNextToken];
}
/**
* Parse an expression starting at the current token. Throws an error if there are no more
* tokens and
*
* @param {number} rbp
* a "right binding power"
* @returns {function} The formatter function for the expression
*/
function expression(rbp) {
var fnLeft;
oToken = advance();
if (!oToken) {
error("Expected expression but instead saw end of input", sInput);
}
fnLeft = mSymbols[oToken.id].nud(oToken, oParser);
while (iNextToken < aTokens.length) {
oToken = current();
if (rbp >= (mSymbols[oToken.id].lbp || 0)) {
break;
}
advance();
fnLeft = mSymbols[oToken.id].led(oToken, oParser, fnLeft);
}
return fnLeft;
}
fnFormatter = expression(0); // do this before calling current() below!
return {
at: current() && current().start,
// call separate function to reduce the closure size of the formatter
formatter: tryCatch(fnFormatter, sInput)
};
}
/**
* The parser to parse expressions in bindings.
*
* @alias sap.ui.base.ExpressionParser
* @private
*/
return {
/**
* Parses a string <code>sInput</code> with an expression based on the syntax sketched
* below.
*
* If a start index <code>iStart</code> for parsing is provided, the input string is parsed
* starting from this index and the return value contains the index after the last
* character belonging to the expression.
*
* The expression syntax is a subset of JavaScript expression syntax with the
* enhancement that the only "variable" parts in an expression are bindings.
* The following expression constructs are supported: <ul>
* <li> String literal enclosed in single or double quotes, e.g. 'foo' </li>
* <li> Null and Boolean literals: null, true, false </li>
* <li> Object and number literals, e.g. {foo:'bar'} and 3.141 </li>
* <li> Grouping, e.g. a * (b + c)</li>
* <li> Unary operators !, +, -, typeof </li>
* <li> Multiplicative Operators: *, /, % </li>
* <li> Additive Operators: +, - </li>
* <li> Relational Operators: <, >, <=, >= </li>
* <li> Strict Equality Operators: ===, !== </li>
* <li> Binary Logical Operators: &&, || </li>
* <li> Conditional Operator: ? : </li>
* <li> Member access via . operator </li>
* <li> Function call </li>
* <li> Embedded binding to refer to model contents, e.g. ${myModel>/Address/city} </li>
* <li> Global functions and objects: encodeURIComponent, Math, RegExp </li>
* <li> Property Access, e.g. ['foo', 'bar'][0] or Math['PI']</li>
* <li> Array literal, e.g. ['foo', 'bar'] </li>
* </ul>
*
* @param {function} fnResolveBinding - the function to resolve embedded bindings
* @param {string} sInput - the string to be parsed
* @param {int} [iStart=0] - the index to start parsing
* @param {object} [mGlobals]
* global variables allowed in the expression as map of variable name to its value;
* note that there is a default map of known global variables
* @param {object} [mLocals={}]
* local variables additionally allowed in the expression (shadowing global ones)
* as map of variable name to its value
* @returns {object} the parse result with the following properties
* result: object with the properties
* formatter: the formatter function to evaluate the expression which
* takes the parts corresponding to bindings embedded in the expression as
* parameters
* parts: the array of parts contained in the expression string which is
* empty if no parts exist
* at: the index of the first character after the expression in sInput
* @throws SyntaxError
* If the expression string is invalid or unsupported. The at property of
* the error contains the position where parsing failed.
*/
parse: function (fnResolveBinding, sInput, iStart, mGlobals, mLocals) {
var oResult, oTokens;
Measurement.average(sPerformanceParse, "", aPerformanceCategories);
oTokens = tokenize(fnResolveBinding, sInput, iStart);
mGlobals = mGlobals || mDefaultGlobals;
if (mLocals) {
mGlobals = Object.assign({}, mGlobals, mLocals);
}
oResult = parse(oTokens.tokens, sInput, mGlobals);
Measurement.end(sPerformanceParse);
if (!oTokens.parts.length) {
return {
constant: oResult.formatter(),
at: oResult.at || oTokens.at
};
}
function formatter() {
//turn separate parameters for parts into one (array like) parameter
return oResult.formatter(arguments);
}
formatter.textFragments = true; //use CompositeBinding even if there is only one part
return {
result: {
formatter: formatter,
parts: oTokens.parts
},
at: oResult.at || oTokens.at
};
},
/**
* An object containing variables that can be used as global variables in an expression.
*/
_globals: mDefaultGlobals
};
}, /* bExport= */ true);