1875 lines
49 KiB
JavaScript
1875 lines
49 KiB
JavaScript
// https://silentmatt.com/javascript-expression-evaluator/
|
||
|
||
.pragma library
|
||
|
||
var INUMBER = 'INUMBER';
|
||
var IOP1 = 'IOP1';
|
||
var IOP2 = 'IOP2';
|
||
var IOP3 = 'IOP3';
|
||
var IVAR = 'IVAR';
|
||
var IVARNAME = 'IVARNAME';
|
||
var IFUNCALL = 'IFUNCALL';
|
||
var IFUNDEF = 'IFUNDEF';
|
||
var IEXPR = 'IEXPR';
|
||
var IEXPREVAL = 'IEXPREVAL';
|
||
var IMEMBER = 'IMEMBER';
|
||
var IENDSTATEMENT = 'IENDSTATEMENT';
|
||
var IARRAY = 'IARRAY';
|
||
|
||
// Additional variable characters.
|
||
var ADDITIONAL_VARCHARS = [
|
||
"α","β","γ","δ","ε","ζ","η",
|
||
"π","θ","κ","λ","μ","ξ","ρ",
|
||
"ς","σ","τ","φ","χ","ψ","ω",
|
||
"Γ","Δ","Θ","Λ","Ξ","Π","Σ",
|
||
"Φ","Ψ","Ω","ₐ","ₑ","ₒ","ₓ",
|
||
"ₕ","ₖ","ₗ","ₘ","ₙ","ₚ","ₛ",
|
||
"ₜ","¹","²","³","⁴","⁵","⁶",
|
||
"⁷","⁸","⁹","⁰","₁","₂","₃",
|
||
"₄","₅","₆","₇","₈","₉","₀"
|
||
]
|
||
|
||
function Instruction(type, value) {
|
||
this.type = type;
|
||
this.value = (value !== undefined && value !== null) ? value : 0;
|
||
}
|
||
|
||
Instruction.prototype.toString = function () {
|
||
switch (this.type) {
|
||
case INUMBER:
|
||
case IOP1:
|
||
case IOP2:
|
||
case IOP3:
|
||
case IVAR:
|
||
case IVARNAME:
|
||
case IENDSTATEMENT:
|
||
return this.value;
|
||
case IFUNCALL:
|
||
return 'CALL ' + this.value;
|
||
case IFUNDEF:
|
||
return 'DEF ' + this.value;
|
||
case IARRAY:
|
||
return 'ARRAY ' + this.value;
|
||
case IMEMBER:
|
||
return '.' + this.value;
|
||
default:
|
||
return 'Invalid Instruction';
|
||
}
|
||
};
|
||
|
||
function unaryInstruction(value) {
|
||
return new Instruction(IOP1, value);
|
||
}
|
||
|
||
function binaryInstruction(value) {
|
||
return new Instruction(IOP2, value);
|
||
}
|
||
|
||
function ternaryInstruction(value) {
|
||
return new Instruction(IOP3, value);
|
||
}
|
||
|
||
function simplify(tokens, unaryOps, binaryOps, ternaryOps, values) {
|
||
var nstack = [];
|
||
var newexpression = [];
|
||
var n1, n2, n3;
|
||
var f;
|
||
for (var i = 0; i < tokens.length; i++) {
|
||
var item = tokens[i];
|
||
var type = item.type;
|
||
if (type === INUMBER || type === IVARNAME) {
|
||
if (Array.isArray(item.value)) {
|
||
nstack.push.apply(nstack, simplify(item.value.map(function (x) {
|
||
return new Instruction(INUMBER, x);
|
||
}).concat(new Instruction(IARRAY, item.value.length)), unaryOps, binaryOps, ternaryOps, values));
|
||
} else {
|
||
nstack.push(item);
|
||
}
|
||
} else if (type === IVAR && values.hasOwnProperty(item.value)) {
|
||
item = new Instruction(INUMBER, values[item.value]);
|
||
nstack.push(item);
|
||
} else if (type === IOP2 && nstack.length > 1) {
|
||
n2 = nstack.pop();
|
||
n1 = nstack.pop();
|
||
f = binaryOps[item.value];
|
||
item = new Instruction(INUMBER, f(n1.value, n2.value));
|
||
nstack.push(item);
|
||
} else if (type === IOP3 && nstack.length > 2) {
|
||
n3 = nstack.pop();
|
||
n2 = nstack.pop();
|
||
n1 = nstack.pop();
|
||
if (item.value === '?') {
|
||
nstack.push(n1.value ? n2.value : n3.value);
|
||
} else {
|
||
f = ternaryOps[item.value];
|
||
item = new Instruction(INUMBER, f(n1.value, n2.value, n3.value));
|
||
nstack.push(item);
|
||
}
|
||
} else if (type === IOP1 && nstack.length > 0) {
|
||
n1 = nstack.pop();
|
||
f = unaryOps[item.value];
|
||
item = new Instruction(INUMBER, f(n1.value));
|
||
nstack.push(item);
|
||
} else if (type === IEXPR) {
|
||
while (nstack.length > 0) {
|
||
newexpression.push(nstack.shift());
|
||
}
|
||
newexpression.push(new Instruction(IEXPR, simplify(item.value, unaryOps, binaryOps, ternaryOps, values)));
|
||
} else if (type === IMEMBER && nstack.length > 0) {
|
||
n1 = nstack.pop();
|
||
//console.log("Getting property ", item.value, "of", n1)
|
||
if(item.value in n1.value)
|
||
nstack.push(new Instruction(INUMBER, n1.value[item.value]));
|
||
else
|
||
throw new Error(qsTranslate('error', 'Cannot find property %1 of object %2.').arg(item.value).arg(n1))
|
||
} /* else if (type === IARRAY && nstack.length >= item.value) {
|
||
var length = item.value;
|
||
while (length-- > 0) {
|
||
newexpression.push(nstack.pop());
|
||
}
|
||
newexpression.push(new Instruction(IARRAY, item.value));
|
||
} */ else {
|
||
while (nstack.length > 0) {
|
||
newexpression.push(nstack.shift());
|
||
}
|
||
newexpression.push(item);
|
||
}
|
||
}
|
||
while (nstack.length > 0) {
|
||
newexpression.push(nstack.shift());
|
||
}
|
||
return newexpression;
|
||
}
|
||
|
||
function substitute(tokens, variable, expr) {
|
||
var newexpression = [];
|
||
for (var i = 0; i < tokens.length; i++) {
|
||
var item = tokens[i];
|
||
var type = item.type;
|
||
if (type === IVAR && item.value === variable) {
|
||
for (var j = 0; j < expr.tokens.length; j++) {
|
||
var expritem = expr.tokens[j];
|
||
var replitem;
|
||
if (expritem.type === IOP1) {
|
||
replitem = unaryInstruction(expritem.value);
|
||
} else if (expritem.type === IOP2) {
|
||
replitem = binaryInstruction(expritem.value);
|
||
} else if (expritem.type === IOP3) {
|
||
replitem = ternaryInstruction(expritem.value);
|
||
} else {
|
||
replitem = new Instruction(expritem.type, expritem.value);
|
||
}
|
||
newexpression.push(replitem);
|
||
}
|
||
} else if (type === IEXPR) {
|
||
newexpression.push(new Instruction(IEXPR, substitute(item.value, variable, expr)));
|
||
} else {
|
||
newexpression.push(item);
|
||
}
|
||
}
|
||
return newexpression;
|
||
}
|
||
|
||
function evaluate(tokens, expr, values) {
|
||
var nstack = [];
|
||
var n1, n2, n3;
|
||
var f, args, argCount;
|
||
|
||
if (isExpressionEvaluator(tokens)) {
|
||
return resolveExpression(tokens, values);
|
||
}
|
||
|
||
var numTokens = tokens.length;
|
||
|
||
for (var i = 0; i < numTokens; i++) {
|
||
var item = tokens[i];
|
||
var type = item.type;
|
||
if (type === INUMBER || type === IVARNAME) {
|
||
nstack.push(item.value);
|
||
} else if (type === IOP2) {
|
||
n2 = nstack.pop();
|
||
n1 = nstack.pop();
|
||
if (item.value === 'and') {
|
||
nstack.push(n1 ? !!evaluate(n2, expr, values) : false);
|
||
} else if (item.value === 'or') {
|
||
nstack.push(n1 ? true : !!evaluate(n2, expr, values));
|
||
} else if (item.value === '=') {
|
||
f = expr.binaryOps[item.value];
|
||
nstack.push(f(n1, evaluate(n2, expr, values), values));
|
||
} else {
|
||
f = expr.binaryOps[item.value];
|
||
nstack.push(f(resolveExpression(n1, values), resolveExpression(n2, values)));
|
||
}
|
||
} else if (type === IOP3) {
|
||
n3 = nstack.pop();
|
||
n2 = nstack.pop();
|
||
n1 = nstack.pop();
|
||
if (item.value === '?') {
|
||
nstack.push(evaluate(n1 ? n2 : n3, expr, values));
|
||
} else {
|
||
f = expr.ternaryOps[item.value];
|
||
nstack.push(f(resolveExpression(n1, values), resolveExpression(n2, values), resolveExpression(n3, values)));
|
||
}
|
||
} else if (type === IVAR) {
|
||
// Check for variable value
|
||
if (/^__proto__|prototype|constructor$/.test(item.value)) {
|
||
throw new Error('WARNING: Prototype access detected and denied. If you downloaded this file from the internet, this file might be a virus.');
|
||
} else if (item.value in expr.functions) {
|
||
nstack.push(expr.functions[item.value]);
|
||
} else if (item.value in expr.unaryOps && expr.parser.isOperatorEnabled(item.value)) {
|
||
nstack.push(expr.unaryOps[item.value]);
|
||
} else {
|
||
var v = values[item.value];
|
||
if (v !== undefined) {
|
||
nstack.push(v);
|
||
} else {
|
||
throw new Error(qsTranslate('error', 'Undefined variable %1.').arg(item.value));
|
||
}
|
||
}
|
||
} else if (type === IOP1) {
|
||
n1 = nstack.pop();
|
||
f = expr.unaryOps[item.value];
|
||
nstack.push(f(resolveExpression(n1, values)));
|
||
} else if (type === IFUNCALL) {
|
||
argCount = item.value;
|
||
args = [];
|
||
while (argCount-- > 0) {
|
||
args.unshift(resolveExpression(nstack.pop(), values));
|
||
}
|
||
f = nstack.pop();
|
||
if (f.apply && f.call) {
|
||
nstack.push(f.apply(undefined, args));
|
||
} else if(f.execute) {
|
||
// Objects & expressions execution
|
||
nstack.push(f.execute.apply(f, args));
|
||
} else {
|
||
throw new Error(qsTranslate('error', '%1 cannot be executed.').arg(f));
|
||
}
|
||
} else if (type === IFUNDEF) {
|
||
// Create closure to keep references to arguments and expression
|
||
nstack.push((function () {
|
||
var n2 = nstack.pop();
|
||
var args = [];
|
||
var argCount = item.value;
|
||
while (argCount-- > 0) {
|
||
args.unshift(nstack.pop());
|
||
}
|
||
var n1 = nstack.pop();
|
||
var f = function () {
|
||
var scope = Object.assign({}, values);
|
||
for (var i = 0, len = args.length; i < len; i++) {
|
||
scope[args[i]] = arguments[i];
|
||
}
|
||
return evaluate(n2, expr, scope);
|
||
};
|
||
// f.name = n1
|
||
Object.defineProperty(f, 'name', {
|
||
value: n1,
|
||
writable: false
|
||
});
|
||
values[n1] = f;
|
||
return f;
|
||
})());
|
||
} else if (type === IEXPR) {
|
||
nstack.push(createExpressionEvaluator(item, expr));
|
||
} else if (type === IEXPREVAL) {
|
||
nstack.push(item);
|
||
} else if (type === IMEMBER) {
|
||
n1 = nstack.pop();
|
||
//console.log("Getting property", item.value, "of", n1,":",n1[item.value])
|
||
if(item.value in n1)
|
||
if(n1[item.value].execute && n1[item.value].cached)
|
||
nstack.push(n1[item.value].execute())
|
||
else
|
||
nstack.push(n1[item.value]);
|
||
else
|
||
throw new Error(qsTranslate('error', 'Cannot find property %1 of object %2.').arg(item.value).arg(n1))
|
||
} else if (type === IENDSTATEMENT) {
|
||
nstack.pop();
|
||
} else if (type === IARRAY) {
|
||
argCount = item.value;
|
||
args = [];
|
||
while (argCount-- > 0) {
|
||
args.unshift(nstack.pop());
|
||
}
|
||
nstack.push(args);
|
||
} else {
|
||
throw new Error(qsTranslate('error', 'Invalid expression.'));
|
||
}
|
||
}
|
||
if (nstack.length > 1) {
|
||
throw new Error(qsTranslate('error', 'Invalid expression (parity).'));
|
||
}
|
||
// Explicitly return zero to avoid test issues caused by -0
|
||
return nstack[0] === 0 ? 0 : resolveExpression(nstack[0], values);
|
||
}
|
||
|
||
function createExpressionEvaluator(token, expr, values) {
|
||
if (isExpressionEvaluator(token)) return token;
|
||
return {
|
||
type: IEXPREVAL,
|
||
value: function (scope) {
|
||
return evaluate(token.value, expr, scope);
|
||
}
|
||
};
|
||
}
|
||
|
||
function isExpressionEvaluator(n) {
|
||
return n && n.type === IEXPREVAL;
|
||
}
|
||
|
||
function resolveExpression(n, values) {
|
||
return isExpressionEvaluator(n) ? n.value(values) : n;
|
||
}
|
||
|
||
function expressionToString(tokens, toJS) {
|
||
var nstack = [];
|
||
var n1, n2, n3;
|
||
var f, args, argCount;
|
||
for (var i = 0; i < tokens.length; i++) {
|
||
var item = tokens[i];
|
||
var type = item.type;
|
||
if (type === INUMBER) {
|
||
if (typeof item.value === 'number' && item.value < 0) {
|
||
nstack.push('(' + item.value + ')');
|
||
} else if (Array.isArray(item.value)) {
|
||
nstack.push('[' + item.value.map(escapeValue).join(', ') + ']');
|
||
} else {
|
||
nstack.push(escapeValue(item.value));
|
||
}
|
||
} else if (type === IOP2) {
|
||
n2 = nstack.pop();
|
||
n1 = nstack.pop();
|
||
f = item.value;
|
||
if (toJS) {
|
||
if (f === '^') {
|
||
nstack.push('Math.pow(' + n1 + ', ' + n2 + ')');
|
||
} else if (f === 'and') {
|
||
nstack.push('(!!' + n1 + ' && !!' + n2 + ')');
|
||
} else if (f === 'or') {
|
||
nstack.push('(!!' + n1 + ' || !!' + n2 + ')');
|
||
} else if (f === '||') {
|
||
nstack.push('(function(a,b){ return Array.isArray(a) && Array.isArray(b) ? a.concat(b) : String(a) + String(b); }((' + n1 + '),(' + n2 + ')))');
|
||
} else if (f === '==') {
|
||
nstack.push('(' + n1 + ' === ' + n2 + ')');
|
||
} else if (f === '!=') {
|
||
nstack.push('(' + n1 + ' !== ' + n2 + ')');
|
||
} else if (f === '[') {
|
||
nstack.push(n1 + '[(' + n2 + ') | 0]');
|
||
} else {
|
||
nstack.push('(' + n1 + ' ' + f + ' ' + n2 + ')');
|
||
}
|
||
} else {
|
||
if (f === '[') {
|
||
nstack.push(n1 + '[' + n2 + ']');
|
||
} else {
|
||
nstack.push('(' + n1 + ' ' + f + ' ' + n2 + ')');
|
||
}
|
||
}
|
||
} else if (type === IOP3) {
|
||
n3 = nstack.pop();
|
||
n2 = nstack.pop();
|
||
n1 = nstack.pop();
|
||
f = item.value;
|
||
if (f === '?') {
|
||
nstack.push('(' + n1 + ' ? ' + n2 + ' : ' + n3 + ')');
|
||
} else {
|
||
throw new Error(qsTranslate('error', 'Invalid expression.'));
|
||
}
|
||
} else if (type === IVAR || type === IVARNAME) {
|
||
nstack.push(item.value);
|
||
} else if (type === IOP1) {
|
||
n1 = nstack.pop();
|
||
f = item.value;
|
||
if (f === '-' || f === '+') {
|
||
nstack.push('(' + f + n1 + ')');
|
||
} else if (toJS) {
|
||
if (f === 'not') {
|
||
nstack.push('(' + '!' + n1 + ')');
|
||
} else if (f === '!') {
|
||
nstack.push('fac(' + n1 + ')');
|
||
} else {
|
||
nstack.push(f + '(' + n1 + ')');
|
||
}
|
||
} else if (f === '!') {
|
||
nstack.push('(' + n1 + '!)');
|
||
} else {
|
||
nstack.push('(' + f + ' ' + n1 + ')');
|
||
}
|
||
} else if (type === IFUNCALL) {
|
||
argCount = item.value;
|
||
args = [];
|
||
while (argCount-- > 0) {
|
||
args.unshift(nstack.pop());
|
||
}
|
||
f = nstack.pop();
|
||
nstack.push(f + '(' + args.join(', ') + ')');
|
||
} else if (type === IFUNDEF) {
|
||
n2 = nstack.pop();
|
||
argCount = item.value;
|
||
args = [];
|
||
while (argCount-- > 0) {
|
||
args.unshift(nstack.pop());
|
||
}
|
||
n1 = nstack.pop();
|
||
if (toJS) {
|
||
nstack.push('(' + n1 + ' = function(' + args.join(', ') + ') { return ' + n2 + ' })');
|
||
} else {
|
||
nstack.push('(' + n1 + '(' + args.join(', ') + ') = ' + n2 + ')');
|
||
}
|
||
} else if (type === IMEMBER) {
|
||
n1 = nstack.pop();
|
||
nstack.push(n1 + '.' + item.value);
|
||
} else if (type === IARRAY) {
|
||
argCount = item.value;
|
||
args = [];
|
||
while (argCount-- > 0) {
|
||
args.unshift(nstack.pop());
|
||
}
|
||
nstack.push('[' + args.join(', ') + ']');
|
||
} else if (type === IEXPR) {
|
||
nstack.push('(' + expressionToString(item.value, toJS) + ')');
|
||
} else if (type === IENDSTATEMENT) ; else {
|
||
throw new Error(qsTranslate('error', 'Invalid expression.'));
|
||
}
|
||
}
|
||
if (nstack.length > 1) {
|
||
if (toJS) {
|
||
nstack = [ nstack.join(',') ];
|
||
} else {
|
||
nstack = [ nstack.join(';') ];
|
||
}
|
||
}
|
||
return String(nstack[0]);
|
||
}
|
||
|
||
function escapeValue(v) {
|
||
if (typeof v === 'string') {
|
||
return JSON.stringify(v).replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
|
||
}
|
||
return v;
|
||
}
|
||
|
||
function contains(array, obj) {
|
||
for (var i = 0; i < array.length; i++) {
|
||
if (array[i] === obj) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function getSymbols(tokens, symbols, options) {
|
||
options = options || {};
|
||
var withMembers = !!options.withMembers;
|
||
var prevVar = null;
|
||
|
||
for (var i = 0; i < tokens.length; i++) {
|
||
var item = tokens[i];
|
||
if (item.type === IVAR || item.type === IVARNAME) {
|
||
if (!withMembers && !contains(symbols, item.value)) {
|
||
symbols.push(item.value);
|
||
} else if (prevVar !== null) {
|
||
if (!contains(symbols, prevVar)) {
|
||
symbols.push(prevVar);
|
||
}
|
||
prevVar = item.value;
|
||
} else {
|
||
prevVar = item.value;
|
||
}
|
||
} else if (item.type === IMEMBER && withMembers && prevVar !== null) {
|
||
prevVar += '.' + item.value;
|
||
} else if (item.type === IEXPR) {
|
||
getSymbols(item.value, symbols, options);
|
||
} else if (prevVar !== null) {
|
||
if (!contains(symbols, prevVar)) {
|
||
symbols.push(prevVar);
|
||
}
|
||
prevVar = null;
|
||
}
|
||
}
|
||
|
||
if (prevVar !== null && !contains(symbols, prevVar)) {
|
||
symbols.push(prevVar);
|
||
}
|
||
}
|
||
|
||
function Expression(tokens, parser) {
|
||
this.tokens = tokens;
|
||
this.parser = parser;
|
||
this.unaryOps = parser.unaryOps;
|
||
this.binaryOps = parser.binaryOps;
|
||
this.ternaryOps = parser.ternaryOps;
|
||
this.functions = parser.functions;
|
||
}
|
||
|
||
Expression.prototype.simplify = function (values) {
|
||
values = values || {};
|
||
return new Expression(simplify(this.tokens, this.unaryOps, this.binaryOps, this.ternaryOps, values), this.parser);
|
||
};
|
||
|
||
Expression.prototype.substitute = function (variable, expr) {
|
||
if (!(expr instanceof Expression)) {
|
||
expr = this.parser.parse(String(expr));
|
||
}
|
||
|
||
return new Expression(substitute(this.tokens, variable, expr), this.parser);
|
||
};
|
||
|
||
Expression.prototype.evaluate = function (values) {
|
||
values = values || {};
|
||
return evaluate(this.tokens, this, values);
|
||
};
|
||
|
||
Expression.prototype.toString = function () {
|
||
return expressionToString(this.tokens, false);
|
||
};
|
||
|
||
Expression.prototype.symbols = function (options) {
|
||
options = options || {};
|
||
var vars = [];
|
||
getSymbols(this.tokens, vars, options);
|
||
return vars;
|
||
};
|
||
|
||
Expression.prototype.variables = function (options) {
|
||
options = options || {};
|
||
var vars = [];
|
||
getSymbols(this.tokens, vars, options);
|
||
var functions = this.functions;
|
||
return vars.filter(function (name) {
|
||
return !(name in functions);
|
||
});
|
||
};
|
||
|
||
Expression.prototype.toJSFunction = function (param, variables) {
|
||
var expr = this;
|
||
var f = new Function(param, 'with(this.functions) with (this.ternaryOps) with (this.binaryOps) with (this.unaryOps) { return ' + expressionToString(this.simplify(variables).tokens, true) + '; }'); // eslint-disable-line no-new-func
|
||
return function () {
|
||
return f.apply(expr, arguments);
|
||
};
|
||
};
|
||
|
||
var TEOF = 'TEOF';
|
||
var TOP = 'TOP';
|
||
var TNUMBER = 'TNUMBER';
|
||
var TSTRING = 'TSTRING';
|
||
var TPAREN = 'TPAREN';
|
||
var TBRACKET = 'TBRACKET';
|
||
var TCOMMA = 'TCOMMA';
|
||
var TNAME = 'TNAME';
|
||
var TSEMICOLON = 'TSEMICOLON';
|
||
|
||
function Token(type, value, index) {
|
||
this.type = type;
|
||
this.value = value;
|
||
this.index = index;
|
||
}
|
||
|
||
Token.prototype.toString = function () {
|
||
return this.type + ': ' + this.value;
|
||
};
|
||
|
||
function TokenStream(parser, expression) {
|
||
this.pos = 0;
|
||
this.current = null;
|
||
this.unaryOps = parser.unaryOps;
|
||
this.binaryOps = parser.binaryOps;
|
||
this.ternaryOps = parser.ternaryOps;
|
||
this.consts = parser.consts;
|
||
this.expression = expression;
|
||
this.savedPosition = 0;
|
||
this.savedCurrent = null;
|
||
this.options = parser.options;
|
||
this.parser = parser;
|
||
}
|
||
|
||
TokenStream.prototype.newToken = function (type, value, pos) {
|
||
return new Token(type, value, pos != null ? pos : this.pos);
|
||
};
|
||
|
||
TokenStream.prototype.save = function () {
|
||
this.savedPosition = this.pos;
|
||
this.savedCurrent = this.current;
|
||
};
|
||
|
||
TokenStream.prototype.restore = function () {
|
||
this.pos = this.savedPosition;
|
||
this.current = this.savedCurrent;
|
||
};
|
||
|
||
TokenStream.prototype.next = function () {
|
||
if (this.pos >= this.expression.length) {
|
||
return this.newToken(TEOF, 'EOF');
|
||
}
|
||
|
||
if (this.isWhitespace() || this.isComment()) {
|
||
return this.next();
|
||
} else if (this.isRadixInteger() ||
|
||
this.isNumber() ||
|
||
this.isOperator() ||
|
||
this.isString() ||
|
||
this.isParen() ||
|
||
this.isBracket() ||
|
||
this.isComma() ||
|
||
this.isSemicolon() ||
|
||
this.isNamedOp() ||
|
||
this.isConst() ||
|
||
this.isName()) {
|
||
return this.current;
|
||
} else {
|
||
this.parseError(qsTranslate('error', 'Unknown character "%1".').arg(this.expression.charAt(this.pos)));
|
||
}
|
||
};
|
||
|
||
TokenStream.prototype.isString = function () {
|
||
var r = false;
|
||
var startPos = this.pos;
|
||
var quote = this.expression.charAt(startPos);
|
||
|
||
if (quote === '\'' || quote === '"') {
|
||
var index = this.expression.indexOf(quote, startPos + 1);
|
||
while (index >= 0 && this.pos < this.expression.length) {
|
||
this.pos = index + 1;
|
||
if (this.expression.charAt(index - 1) !== '\\') {
|
||
var rawString = this.expression.substring(startPos + 1, index);
|
||
this.current = this.newToken(TSTRING, this.unescape(rawString), startPos);
|
||
r = true;
|
||
break;
|
||
}
|
||
index = this.expression.indexOf(quote, index + 1);
|
||
}
|
||
}
|
||
return r;
|
||
};
|
||
|
||
TokenStream.prototype.isParen = function () {
|
||
var c = this.expression.charAt(this.pos);
|
||
if (c === '(' || c === ')') {
|
||
this.current = this.newToken(TPAREN, c);
|
||
this.pos++;
|
||
return true;
|
||
}
|
||
return false;
|
||
};
|
||
|
||
TokenStream.prototype.isBracket = function () {
|
||
var c = this.expression.charAt(this.pos);
|
||
if ((c === '[' || c === ']') && this.isOperatorEnabled('[')) {
|
||
this.current = this.newToken(TBRACKET, c);
|
||
this.pos++;
|
||
return true;
|
||
}
|
||
return false;
|
||
};
|
||
|
||
TokenStream.prototype.isComma = function () {
|
||
var c = this.expression.charAt(this.pos);
|
||
if (c === ',') {
|
||
this.current = this.newToken(TCOMMA, ',');
|
||
this.pos++;
|
||
return true;
|
||
}
|
||
return false;
|
||
};
|
||
|
||
TokenStream.prototype.isSemicolon = function () {
|
||
var c = this.expression.charAt(this.pos);
|
||
if (c === ';') {
|
||
this.current = this.newToken(TSEMICOLON, ';');
|
||
this.pos++;
|
||
return true;
|
||
}
|
||
return false;
|
||
};
|
||
|
||
TokenStream.prototype.isConst = function () {
|
||
var startPos = this.pos;
|
||
var i = startPos;
|
||
for (; i < this.expression.length; i++) {
|
||
var c = this.expression.charAt(i);
|
||
if (c.toUpperCase() === c.toLowerCase()) {
|
||
if (i === this.pos || (c !== '_' && c !== '.' && (c < '0' || c > '9'))) {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (i > startPos) {
|
||
var str = this.expression.substring(startPos, i);
|
||
if (str in this.consts) {
|
||
this.current = this.newToken(TNUMBER, this.consts[str]);
|
||
this.pos += str.length;
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
};
|
||
|
||
TokenStream.prototype.isNamedOp = function () {
|
||
var startPos = this.pos;
|
||
var i = startPos;
|
||
for (; i < this.expression.length; i++) {
|
||
var c = this.expression.charAt(i);
|
||
if (c.toUpperCase() === c.toLowerCase()) {
|
||
if (i === this.pos || (c !== '_' && (c < '0' || c > '9'))) {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (i > startPos) {
|
||
var str = this.expression.substring(startPos, i);
|
||
if (this.isOperatorEnabled(str) && (str in this.binaryOps || str in this.unaryOps || str in this.ternaryOps)) {
|
||
this.current = this.newToken(TOP, str);
|
||
this.pos += str.length;
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
};
|
||
|
||
TokenStream.prototype.isName = function () {
|
||
var startPos = this.pos;
|
||
var i = startPos;
|
||
var hasLetter = false;
|
||
for (; i < this.expression.length; i++) {
|
||
var c = this.expression.charAt(i);
|
||
if (c.toUpperCase() === c.toLowerCase() && !ADDITIONAL_VARCHARS.includes(c)) {
|
||
if (i === this.pos && (c === '$' || c === '_')) {
|
||
if (c === '_') {
|
||
hasLetter = true;
|
||
}
|
||
continue;
|
||
} else if (i === this.pos || !hasLetter || (c !== '_' && (c < '0' || c > '9'))) {
|
||
break;
|
||
}
|
||
} else {
|
||
hasLetter = true;
|
||
}
|
||
}
|
||
if (hasLetter) {
|
||
var str = this.expression.substring(startPos, i);
|
||
this.current = this.newToken(TNAME, str);
|
||
this.pos += str.length;
|
||
return true;
|
||
}
|
||
return false;
|
||
};
|
||
|
||
TokenStream.prototype.isWhitespace = function () {
|
||
var r = false;
|
||
var c = this.expression.charAt(this.pos);
|
||
while (c === ' ' || c === '\t' || c === '\n' || c === '\r') {
|
||
r = true;
|
||
this.pos++;
|
||
if (this.pos >= this.expression.length) {
|
||
break;
|
||
}
|
||
c = this.expression.charAt(this.pos);
|
||
}
|
||
return r;
|
||
};
|
||
|
||
var codePointPattern = /^[0-9a-f]{4}$/i;
|
||
|
||
TokenStream.prototype.unescape = function (v) {
|
||
var index = v.indexOf('\\');
|
||
if (index < 0) {
|
||
return v;
|
||
}
|
||
|
||
var buffer = v.substring(0, index);
|
||
while (index >= 0) {
|
||
var c = v.charAt(++index);
|
||
switch (c) {
|
||
case '\'':
|
||
buffer += '\'';
|
||
break;
|
||
case '"':
|
||
buffer += '"';
|
||
break;
|
||
case '\\':
|
||
buffer += '\\';
|
||
break;
|
||
case '/':
|
||
buffer += '/';
|
||
break;
|
||
case 'b':
|
||
buffer += '\b';
|
||
break;
|
||
case 'f':
|
||
buffer += '\f';
|
||
break;
|
||
case 'n':
|
||
buffer += '\n';
|
||
break;
|
||
case 'r':
|
||
buffer += '\r';
|
||
break;
|
||
case 't':
|
||
buffer += '\t';
|
||
break;
|
||
case 'u':
|
||
// interpret the following 4 characters as the hex of the unicode code point
|
||
var codePoint = v.substring(index + 1, index + 5);
|
||
if (!codePointPattern.test(codePoint)) {
|
||
this.parseError(qsTranslate('error', 'Illegal escape sequence: %1.').arg("\\u" + codePoint));
|
||
}
|
||
buffer += String.fromCharCode(parseInt(codePoint, 16));
|
||
index += 4;
|
||
break;
|
||
default:
|
||
throw this.parseError(qsTranslate('error', 'Illegal escape sequence: %1.').arg('\\' + c));
|
||
}
|
||
++index;
|
||
var backslash = v.indexOf('\\', index);
|
||
buffer += v.substring(index, backslash < 0 ? v.length : backslash);
|
||
index = backslash;
|
||
}
|
||
|
||
return buffer;
|
||
};
|
||
|
||
TokenStream.prototype.isComment = function () {
|
||
var c = this.expression.charAt(this.pos);
|
||
if (c === '/' && this.expression.charAt(this.pos + 1) === '*') {
|
||
this.pos = this.expression.indexOf('*/', this.pos) + 2;
|
||
if (this.pos === 1) {
|
||
this.pos = this.expression.length;
|
||
}
|
||
return true;
|
||
}
|
||
return false;
|
||
};
|
||
|
||
TokenStream.prototype.isRadixInteger = function () {
|
||
var pos = this.pos;
|
||
|
||
if (pos >= this.expression.length - 2 || this.expression.charAt(pos) !== '0') {
|
||
return false;
|
||
}
|
||
++pos;
|
||
|
||
var radix;
|
||
var validDigit;
|
||
if (this.expression.charAt(pos) === 'x') {
|
||
radix = 16;
|
||
validDigit = /^[0-9a-f]$/i;
|
||
++pos;
|
||
} else if (this.expression.charAt(pos) === 'b') {
|
||
radix = 2;
|
||
validDigit = /^[01]$/i;
|
||
++pos;
|
||
} else {
|
||
return false;
|
||
}
|
||
|
||
var valid = false;
|
||
var startPos = pos;
|
||
|
||
while (pos < this.expression.length) {
|
||
var c = this.expression.charAt(pos);
|
||
if (validDigit.test(c)) {
|
||
pos++;
|
||
valid = true;
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (valid) {
|
||
this.current = this.newToken(TNUMBER, parseInt(this.expression.substring(startPos, pos), radix));
|
||
this.pos = pos;
|
||
}
|
||
return valid;
|
||
};
|
||
|
||
TokenStream.prototype.isNumber = function () {
|
||
var valid = false;
|
||
var pos = this.pos;
|
||
var startPos = pos;
|
||
var resetPos = pos;
|
||
var foundDot = false;
|
||
var foundDigits = false;
|
||
var c;
|
||
|
||
while (pos < this.expression.length) {
|
||
c = this.expression.charAt(pos);
|
||
if ((c >= '0' && c <= '9') || (!foundDot && c === '.')) {
|
||
if (c === '.') {
|
||
foundDot = true;
|
||
} else {
|
||
foundDigits = true;
|
||
}
|
||
pos++;
|
||
valid = foundDigits;
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (valid) {
|
||
resetPos = pos;
|
||
}
|
||
|
||
if (c === 'e' || c === 'E') {
|
||
pos++;
|
||
var acceptSign = true;
|
||
var validExponent = false;
|
||
while (pos < this.expression.length) {
|
||
c = this.expression.charAt(pos);
|
||
if (acceptSign && (c === '+' || c === '-')) {
|
||
acceptSign = false;
|
||
} else if (c >= '0' && c <= '9') {
|
||
validExponent = true;
|
||
acceptSign = false;
|
||
} else {
|
||
break;
|
||
}
|
||
pos++;
|
||
}
|
||
|
||
if (!validExponent) {
|
||
pos = resetPos;
|
||
}
|
||
}
|
||
|
||
if (valid) {
|
||
this.current = this.newToken(TNUMBER, parseFloat(this.expression.substring(startPos, pos)));
|
||
this.pos = pos;
|
||
} else {
|
||
this.pos = resetPos;
|
||
}
|
||
return valid;
|
||
};
|
||
|
||
TokenStream.prototype.isOperator = function () {
|
||
var startPos = this.pos;
|
||
var c = this.expression.charAt(this.pos);
|
||
|
||
if (c === '+' || c === '-' || c === '*' || c === '/' || c === '%' || c === '^' || c === '?' || c === ':' || c === '.') {
|
||
this.current = this.newToken(TOP, c);
|
||
} else if (c === '∙' || c === '•') {
|
||
this.current = this.newToken(TOP, '*');
|
||
} else if (c === '>') {
|
||
if (this.expression.charAt(this.pos + 1) === '=') {
|
||
this.current = this.newToken(TOP, '>=');
|
||
this.pos++;
|
||
} else {
|
||
this.current = this.newToken(TOP, '>');
|
||
}
|
||
} else if (c === '<') {
|
||
if (this.expression.charAt(this.pos + 1) === '=') {
|
||
this.current = this.newToken(TOP, '<=');
|
||
this.pos++;
|
||
} else {
|
||
this.current = this.newToken(TOP, '<');
|
||
}
|
||
} else if (c === '|') {
|
||
if (this.expression.charAt(this.pos + 1) === '|') {
|
||
this.current = this.newToken(TOP, '||');
|
||
this.pos++;
|
||
} else {
|
||
return false;
|
||
}
|
||
} else if (c === '=') {
|
||
if (this.expression.charAt(this.pos + 1) === '=') {
|
||
this.current = this.newToken(TOP, '==');
|
||
this.pos++;
|
||
} else {
|
||
this.current = this.newToken(TOP, c);
|
||
}
|
||
} else if (c === '!') {
|
||
if (this.expression.charAt(this.pos + 1) === '=') {
|
||
this.current = this.newToken(TOP, '!=');
|
||
this.pos++;
|
||
} else {
|
||
this.current = this.newToken(TOP, c);
|
||
}
|
||
} else {
|
||
return false;
|
||
}
|
||
this.pos++;
|
||
|
||
if (this.isOperatorEnabled(this.current.value)) {
|
||
return true;
|
||
} else {
|
||
this.pos = startPos;
|
||
return false;
|
||
}
|
||
};
|
||
|
||
TokenStream.prototype.isOperatorEnabled = function (op) {
|
||
return this.parser.isOperatorEnabled(op);
|
||
};
|
||
|
||
TokenStream.prototype.getCoordinates = function () {
|
||
var line = 0;
|
||
var column;
|
||
var newline = -1;
|
||
do {
|
||
line++;
|
||
column = this.pos - newline;
|
||
newline = this.expression.indexOf('\n', newline + 1);
|
||
} while (newline >= 0 && newline < this.pos);
|
||
|
||
return {
|
||
line: line,
|
||
column: column
|
||
};
|
||
};
|
||
|
||
TokenStream.prototype.parseError = function (msg) {
|
||
var coords = this.getCoordinates();
|
||
throw new Error(qsTranslate('error', 'Parse error [%1:%2]: %3').arg(coords.line).arg(coords.column).arg(msg));
|
||
};
|
||
|
||
function ParserState(parser, tokenStream, options) {
|
||
this.parser = parser;
|
||
this.tokens = tokenStream;
|
||
this.current = null;
|
||
this.nextToken = null;
|
||
this.next();
|
||
this.savedCurrent = null;
|
||
this.savedNextToken = null;
|
||
this.allowMemberAccess = options.allowMemberAccess !== false;
|
||
}
|
||
|
||
ParserState.prototype.next = function () {
|
||
this.current = this.nextToken;
|
||
return (this.nextToken = this.tokens.next());
|
||
};
|
||
|
||
ParserState.prototype.tokenMatches = function (token, value) {
|
||
if (typeof value === 'undefined') {
|
||
return true;
|
||
} else if (Array.isArray(value)) {
|
||
return contains(value, token.value);
|
||
} else if (typeof value === 'function') {
|
||
return value(token);
|
||
} else {
|
||
return token.value === value;
|
||
}
|
||
};
|
||
|
||
ParserState.prototype.save = function () {
|
||
this.savedCurrent = this.current;
|
||
this.savedNextToken = this.nextToken;
|
||
this.tokens.save();
|
||
};
|
||
|
||
ParserState.prototype.restore = function () {
|
||
this.tokens.restore();
|
||
this.current = this.savedCurrent;
|
||
this.nextToken = this.savedNextToken;
|
||
};
|
||
|
||
ParserState.prototype.accept = function (type, value) {
|
||
if (this.nextToken.type === type && this.tokenMatches(this.nextToken, value)) {
|
||
this.next();
|
||
return true;
|
||
}
|
||
return false;
|
||
};
|
||
|
||
ParserState.prototype.expect = function (type, value) {
|
||
if (!this.accept(type, value)) {
|
||
var coords = this.tokens.getCoordinates();
|
||
throw new Error(qsTranslate('error', 'Parse error [%1:%2]: %3')
|
||
.arg(coords.line).arg(coords.column)
|
||
.arg(qsTranslate('error', 'Expected %1').arg(value || type)));
|
||
}
|
||
};
|
||
|
||
ParserState.prototype.parseAtom = function (instr) {
|
||
var unaryOps = this.tokens.unaryOps;
|
||
function isPrefixOperator(token) {
|
||
return token.value in unaryOps;
|
||
}
|
||
|
||
if (this.accept(TNAME) || this.accept(TOP, isPrefixOperator)) {
|
||
instr.push(new Instruction(IVAR, this.current.value));
|
||
} else if (this.accept(TNUMBER)) {
|
||
instr.push(new Instruction(INUMBER, this.current.value));
|
||
} else if (this.accept(TSTRING)) {
|
||
instr.push(new Instruction(INUMBER, this.current.value));
|
||
} else if (this.accept(TPAREN, '(')) {
|
||
this.parseExpression(instr);
|
||
this.expect(TPAREN, ')');
|
||
} else if (this.accept(TBRACKET, '[')) {
|
||
if (this.accept(TBRACKET, ']')) {
|
||
instr.push(new Instruction(IARRAY, 0));
|
||
} else {
|
||
var argCount = this.parseArrayList(instr);
|
||
instr.push(new Instruction(IARRAY, argCount));
|
||
}
|
||
} else {
|
||
throw new Error(qsTranslate('error', 'Unexpected %1').arg(this.nextToken));
|
||
}
|
||
};
|
||
|
||
ParserState.prototype.parseExpression = function (instr) {
|
||
var exprInstr = [];
|
||
if (this.parseUntilEndStatement(instr, exprInstr)) {
|
||
return;
|
||
}
|
||
this.parseVariableAssignmentExpression(exprInstr);
|
||
if (this.parseUntilEndStatement(instr, exprInstr)) {
|
||
return;
|
||
}
|
||
this.pushExpression(instr, exprInstr);
|
||
};
|
||
|
||
ParserState.prototype.pushExpression = function (instr, exprInstr) {
|
||
for (var i = 0, len = exprInstr.length; i < len; i++) {
|
||
instr.push(exprInstr[i]);
|
||
}
|
||
};
|
||
|
||
ParserState.prototype.parseUntilEndStatement = function (instr, exprInstr) {
|
||
if (!this.accept(TSEMICOLON)) return false;
|
||
if (this.nextToken && this.nextToken.type !== TEOF && !(this.nextToken.type === TPAREN && this.nextToken.value === ')')) {
|
||
exprInstr.push(new Instruction(IENDSTATEMENT));
|
||
}
|
||
if (this.nextToken.type !== TEOF) {
|
||
this.parseExpression(exprInstr);
|
||
}
|
||
instr.push(new Instruction(IEXPR, exprInstr));
|
||
return true;
|
||
};
|
||
|
||
ParserState.prototype.parseArrayList = function (instr) {
|
||
var argCount = 0;
|
||
|
||
while (!this.accept(TBRACKET, ']')) {
|
||
this.parseExpression(instr);
|
||
++argCount;
|
||
while (this.accept(TCOMMA)) {
|
||
this.parseExpression(instr);
|
||
++argCount;
|
||
}
|
||
}
|
||
|
||
return argCount;
|
||
};
|
||
|
||
ParserState.prototype.parseVariableAssignmentExpression = function (instr) {
|
||
this.parseConditionalExpression(instr);
|
||
while (this.accept(TOP, '=')) {
|
||
var varName = instr.pop();
|
||
var varValue = [];
|
||
var lastInstrIndex = instr.length - 1;
|
||
if (varName.type === IFUNCALL) {
|
||
if (!this.tokens.isOperatorEnabled('()=')) {
|
||
throw new Error(qsTranslate('error', 'Function definition is not permitted.'));
|
||
}
|
||
for (var i = 0, len = varName.value + 1; i < len; i++) {
|
||
var index = lastInstrIndex - i;
|
||
if (instr[index].type === IVAR) {
|
||
instr[index] = new Instruction(IVARNAME, instr[index].value);
|
||
}
|
||
}
|
||
this.parseVariableAssignmentExpression(varValue);
|
||
instr.push(new Instruction(IEXPR, varValue));
|
||
instr.push(new Instruction(IFUNDEF, varName.value));
|
||
continue;
|
||
}
|
||
if (varName.type !== IVAR && varName.type !== IMEMBER) {
|
||
throw new Error(qsTranslate('error', 'Expected variable for assignment.'));
|
||
}
|
||
this.parseVariableAssignmentExpression(varValue);
|
||
instr.push(new Instruction(IVARNAME, varName.value));
|
||
instr.push(new Instruction(IEXPR, varValue));
|
||
instr.push(binaryInstruction('='));
|
||
}
|
||
};
|
||
|
||
ParserState.prototype.parseConditionalExpression = function (instr) {
|
||
this.parseOrExpression(instr);
|
||
while (this.accept(TOP, '?')) {
|
||
var trueBranch = [];
|
||
var falseBranch = [];
|
||
this.parseConditionalExpression(trueBranch);
|
||
this.expect(TOP, ':');
|
||
this.parseConditionalExpression(falseBranch);
|
||
instr.push(new Instruction(IEXPR, trueBranch));
|
||
instr.push(new Instruction(IEXPR, falseBranch));
|
||
instr.push(ternaryInstruction('?'));
|
||
}
|
||
};
|
||
|
||
ParserState.prototype.parseOrExpression = function (instr) {
|
||
this.parseAndExpression(instr);
|
||
while (this.accept(TOP, 'or')) {
|
||
var falseBranch = [];
|
||
this.parseAndExpression(falseBranch);
|
||
instr.push(new Instruction(IEXPR, falseBranch));
|
||
instr.push(binaryInstruction('or'));
|
||
}
|
||
};
|
||
|
||
ParserState.prototype.parseAndExpression = function (instr) {
|
||
this.parseComparison(instr);
|
||
while (this.accept(TOP, 'and')) {
|
||
var trueBranch = [];
|
||
this.parseComparison(trueBranch);
|
||
instr.push(new Instruction(IEXPR, trueBranch));
|
||
instr.push(binaryInstruction('and'));
|
||
}
|
||
};
|
||
|
||
var COMPARISON_OPERATORS = ['==', '!=', '<', '<=', '>=', '>', 'in'];
|
||
|
||
ParserState.prototype.parseComparison = function (instr) {
|
||
this.parseAddSub(instr);
|
||
while (this.accept(TOP, COMPARISON_OPERATORS)) {
|
||
var op = this.current;
|
||
this.parseAddSub(instr);
|
||
instr.push(binaryInstruction(op.value));
|
||
}
|
||
};
|
||
|
||
var ADD_SUB_OPERATORS = ['+', '-', '||'];
|
||
|
||
ParserState.prototype.parseAddSub = function (instr) {
|
||
this.parseTerm(instr);
|
||
while (this.accept(TOP, ADD_SUB_OPERATORS)) {
|
||
var op = this.current;
|
||
this.parseTerm(instr);
|
||
instr.push(binaryInstruction(op.value));
|
||
}
|
||
};
|
||
|
||
var TERM_OPERATORS = ['*', '/', '%'];
|
||
|
||
ParserState.prototype.parseTerm = function (instr) {
|
||
this.parseFactor(instr);
|
||
while (this.accept(TOP, TERM_OPERATORS)) {
|
||
var op = this.current;
|
||
this.parseFactor(instr);
|
||
instr.push(binaryInstruction(op.value));
|
||
}
|
||
};
|
||
|
||
ParserState.prototype.parseFactor = function (instr) {
|
||
var unaryOps = this.tokens.unaryOps;
|
||
function isPrefixOperator(token) {
|
||
return token.value in unaryOps;
|
||
}
|
||
|
||
this.save();
|
||
if (this.accept(TOP, isPrefixOperator)) {
|
||
if (this.current.value !== '-' && this.current.value !== '+') {
|
||
if (this.nextToken.type === TPAREN && this.nextToken.value === '(') {
|
||
this.restore();
|
||
this.parseExponential(instr);
|
||
return;
|
||
} else if (this.nextToken.type === TSEMICOLON || this.nextToken.type === TCOMMA || this.nextToken.type === TEOF || (this.nextToken.type === TPAREN && this.nextToken.value === ')')) {
|
||
this.restore();
|
||
this.parseAtom(instr);
|
||
return;
|
||
}
|
||
}
|
||
|
||
var op = this.current;
|
||
this.parseFactor(instr);
|
||
instr.push(unaryInstruction(op.value));
|
||
} else {
|
||
this.parseExponential(instr);
|
||
}
|
||
};
|
||
|
||
ParserState.prototype.parseExponential = function (instr) {
|
||
this.parsePostfixExpression(instr);
|
||
while (this.accept(TOP, '^')) {
|
||
this.parseFactor(instr);
|
||
instr.push(binaryInstruction('^'));
|
||
}
|
||
};
|
||
|
||
ParserState.prototype.parsePostfixExpression = function (instr) {
|
||
this.parseFunctionCall(instr);
|
||
while (this.accept(TOP, '!')) {
|
||
instr.push(unaryInstruction('!'));
|
||
}
|
||
};
|
||
|
||
ParserState.prototype.parseFunctionCall = function (instr) {
|
||
var unaryOps = this.tokens.unaryOps;
|
||
function isPrefixOperator(token) {
|
||
return token.value in unaryOps;
|
||
}
|
||
|
||
if (this.accept(TOP, isPrefixOperator)) {
|
||
var op = this.current;
|
||
this.parseAtom(instr);
|
||
instr.push(unaryInstruction(op.value));
|
||
} else {
|
||
this.parseMemberExpression(instr);
|
||
while (this.accept(TPAREN, '(')) {
|
||
if (this.accept(TPAREN, ')')) {
|
||
instr.push(new Instruction(IFUNCALL, 0));
|
||
} else {
|
||
var argCount = this.parseArgumentList(instr);
|
||
instr.push(new Instruction(IFUNCALL, argCount));
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
ParserState.prototype.parseArgumentList = function (instr) {
|
||
var argCount = 0;
|
||
|
||
while (!this.accept(TPAREN, ')')) {
|
||
this.parseExpression(instr);
|
||
++argCount;
|
||
while (this.accept(TCOMMA)) {
|
||
this.parseExpression(instr);
|
||
++argCount;
|
||
}
|
||
}
|
||
|
||
return argCount;
|
||
};
|
||
|
||
ParserState.prototype.parseMemberExpression = function (instr) {
|
||
this.parseAtom(instr);
|
||
while (this.accept(TOP, '.') || this.accept(TBRACKET, '[')) {
|
||
var op = this.current;
|
||
|
||
if (op.value === '.') {
|
||
if (!this.allowMemberAccess) {
|
||
throw new Error(qsTranslate('error', 'Unexpected ".": member access is not permitted'));
|
||
}
|
||
|
||
this.expect(TNAME);
|
||
instr.push(new Instruction(IMEMBER, this.current.value));
|
||
} else if (op.value === '[') {
|
||
if (!this.tokens.isOperatorEnabled('[')) {
|
||
throw new Error(qsTranslate('error', 'Unexpected "[]": arrays are disabled.'));
|
||
}
|
||
|
||
this.parseExpression(instr);
|
||
this.expect(TBRACKET, ']');
|
||
instr.push(binaryInstruction('['));
|
||
} else {
|
||
throw new Error(qsTranslate('error', 'Unexpected symbol: %1.').arg(op.value));
|
||
}
|
||
}
|
||
};
|
||
|
||
function add(a, b) {
|
||
return Number(a) + Number(b);
|
||
}
|
||
|
||
function sub(a, b) {
|
||
return a - b;
|
||
}
|
||
|
||
function mul(a, b) {
|
||
return a * b;
|
||
}
|
||
|
||
function div(a, b) {
|
||
return a / b;
|
||
}
|
||
|
||
function mod(a, b) {
|
||
return a % b;
|
||
}
|
||
|
||
function concat(a, b) {
|
||
if (Array.isArray(a) && Array.isArray(b)) {
|
||
return a.concat(b);
|
||
}
|
||
return '' + a + b;
|
||
}
|
||
|
||
function equal(a, b) {
|
||
return a === b;
|
||
}
|
||
|
||
function notEqual(a, b) {
|
||
return a !== b;
|
||
}
|
||
|
||
function greaterThan(a, b) {
|
||
return a > b;
|
||
}
|
||
|
||
function lessThan(a, b) {
|
||
return a < b;
|
||
}
|
||
|
||
function greaterThanEqual(a, b) {
|
||
return a >= b;
|
||
}
|
||
|
||
function lessThanEqual(a, b) {
|
||
return a <= b;
|
||
}
|
||
|
||
function andOperator(a, b) {
|
||
return Boolean(a && b);
|
||
}
|
||
|
||
function orOperator(a, b) {
|
||
return Boolean(a || b);
|
||
}
|
||
|
||
function inOperator(a, b) {
|
||
return contains(b, a);
|
||
}
|
||
|
||
function sinh(a) {
|
||
return ((Math.exp(a) - Math.exp(-a)) / 2);
|
||
}
|
||
|
||
function cosh(a) {
|
||
return ((Math.exp(a) + Math.exp(-a)) / 2);
|
||
}
|
||
|
||
function tanh(a) {
|
||
if (a === Infinity) return 1;
|
||
if (a === -Infinity) return -1;
|
||
return (Math.exp(a) - Math.exp(-a)) / (Math.exp(a) + Math.exp(-a));
|
||
}
|
||
|
||
function asinh(a) {
|
||
if (a === -Infinity) return a;
|
||
return Math.log(a + Math.sqrt((a * a) + 1));
|
||
}
|
||
|
||
function acosh(a) {
|
||
return Math.log(a + Math.sqrt((a * a) - 1));
|
||
}
|
||
|
||
function atanh(a) {
|
||
return (Math.log((1 + a) / (1 - a)) / 2);
|
||
}
|
||
|
||
function log10(a) {
|
||
return Math.log(a) * Math.LOG10E;
|
||
}
|
||
|
||
function neg(a) {
|
||
return -a;
|
||
}
|
||
|
||
function not(a) {
|
||
return !a;
|
||
}
|
||
|
||
function trunc(a) {
|
||
return a < 0 ? Math.ceil(a) : Math.floor(a);
|
||
}
|
||
|
||
function random(a) {
|
||
return Math.random() * (a || 1);
|
||
}
|
||
|
||
function factorial(a) { // a!
|
||
return gamma(a + 1);
|
||
}
|
||
|
||
function isInteger(value) {
|
||
return isFinite(value) && (value === Math.round(value));
|
||
}
|
||
|
||
var GAMMA_G = 4.7421875;
|
||
var GAMMA_P = [
|
||
0.99999999999999709182,
|
||
57.156235665862923517, -59.597960355475491248,
|
||
14.136097974741747174, -0.49191381609762019978,
|
||
0.33994649984811888699e-4,
|
||
0.46523628927048575665e-4, -0.98374475304879564677e-4,
|
||
0.15808870322491248884e-3, -0.21026444172410488319e-3,
|
||
0.21743961811521264320e-3, -0.16431810653676389022e-3,
|
||
0.84418223983852743293e-4, -0.26190838401581408670e-4,
|
||
0.36899182659531622704e-5
|
||
];
|
||
|
||
// Gamma function from math.js
|
||
function gamma(n) {
|
||
var t, x;
|
||
|
||
if (isInteger(n)) {
|
||
if (n <= 0) {
|
||
return isFinite(n) ? Infinity : NaN;
|
||
}
|
||
|
||
if (n > 171) {
|
||
return Infinity; // Will overflow
|
||
}
|
||
|
||
var value = n - 2;
|
||
var res = n - 1;
|
||
while (value > 1) {
|
||
res *= value;
|
||
value--;
|
||
}
|
||
|
||
if (res === 0) {
|
||
res = 1; // 0! is per definition 1
|
||
}
|
||
|
||
return res;
|
||
}
|
||
|
||
if (n < 0.5) {
|
||
return Math.PI / (Math.sin(Math.PI * n) * gamma(1 - n));
|
||
}
|
||
|
||
if (n >= 171.35) {
|
||
return Infinity; // will overflow
|
||
}
|
||
|
||
if (n > 85.0) { // Extended Stirling Approx
|
||
var twoN = n * n;
|
||
var threeN = twoN * n;
|
||
var fourN = threeN * n;
|
||
var fiveN = fourN * n;
|
||
return Math.sqrt(2 * Math.PI / n) * Math.pow((n / Math.E), n) *
|
||
(1 + (1 / (12 * n)) + (1 / (288 * twoN)) - (139 / (51840 * threeN)) -
|
||
(571 / (2488320 * fourN)) + (163879 / (209018880 * fiveN)) +
|
||
(5246819 / (75246796800 * fiveN * n)));
|
||
}
|
||
|
||
--n;
|
||
x = GAMMA_P[0];
|
||
for (var i = 1; i < GAMMA_P.length; ++i) {
|
||
x += GAMMA_P[i] / (n + i);
|
||
}
|
||
|
||
t = n + GAMMA_G + 0.5;
|
||
return Math.sqrt(2 * Math.PI) * Math.pow(t, n + 0.5) * Math.exp(-t) * x;
|
||
}
|
||
|
||
function stringOrArrayLength(s) {
|
||
if (Array.isArray(s)) {
|
||
return s.length;
|
||
}
|
||
return String(s).length;
|
||
}
|
||
|
||
function hypot() {
|
||
var sum = 0;
|
||
var larg = 0;
|
||
for (var i = 0; i < arguments.length; i++) {
|
||
var arg = Math.abs(arguments[i]);
|
||
var div;
|
||
if (larg < arg) {
|
||
div = larg / arg;
|
||
sum = (sum * div * div) + 1;
|
||
larg = arg;
|
||
} else if (arg > 0) {
|
||
div = arg / larg;
|
||
sum += div * div;
|
||
} else {
|
||
sum += arg;
|
||
}
|
||
}
|
||
return larg === Infinity ? Infinity : larg * Math.sqrt(sum);
|
||
}
|
||
|
||
function condition(cond, yep, nope) {
|
||
return cond ? yep : nope;
|
||
}
|
||
|
||
/**
|
||
* Decimal adjustment of a number.
|
||
* From @escopecz.
|
||
*
|
||
* @param {Number} value The number.
|
||
* @param {Integer} exp The exponent (the 10 logarithm of the adjustment base).
|
||
* @return {Number} The adjusted value.
|
||
*/
|
||
function roundTo(value, exp) {
|
||
// If the exp is undefined or zero...
|
||
if (typeof exp === 'undefined' || +exp === 0) {
|
||
return Math.round(value);
|
||
}
|
||
value = +value;
|
||
exp = -(+exp);
|
||
// If the value is not a number or the exp is not an integer...
|
||
if (isNaN(value) || !(typeof exp === 'number' && exp % 1 === 0)) {
|
||
return NaN;
|
||
}
|
||
// Shift
|
||
value = value.toString().split('e');
|
||
value = Math.round(+(value[0] + 'e' + (value[1] ? (+value[1] - exp) : -exp)));
|
||
// Shift back
|
||
value = value.toString().split('e');
|
||
return +(value[0] + 'e' + (value[1] ? (+value[1] + exp) : exp));
|
||
}
|
||
|
||
function setVar(name, value, variables) {
|
||
if (variables) variables[name] = value;
|
||
return value;
|
||
}
|
||
|
||
function arrayIndex(array, index) {
|
||
return array[index | 0];
|
||
}
|
||
|
||
function max(array) {
|
||
if (arguments.length === 1 && Array.isArray(array)) {
|
||
return Math.max.apply(Math, array);
|
||
} else {
|
||
return Math.max.apply(Math, arguments);
|
||
}
|
||
}
|
||
|
||
function min(array) {
|
||
if (arguments.length === 1 && Array.isArray(array)) {
|
||
return Math.min.apply(Math, array);
|
||
} else {
|
||
return Math.min.apply(Math, arguments);
|
||
}
|
||
}
|
||
|
||
function arrayMap(f, a) {
|
||
if (typeof f !== 'function') {
|
||
throw new EvalError(qsTranslate('error', 'First argument to map is not a function.'));
|
||
}
|
||
if (!Array.isArray(a)) {
|
||
throw new EvalError(qsTranslate('error', 'Second argument to map is not an array.'));
|
||
}
|
||
return a.map(function (x, i) {
|
||
return f(x, i);
|
||
});
|
||
}
|
||
|
||
function arrayFold(f, init, a) {
|
||
if (typeof f !== 'function') {
|
||
throw new EvalError(qsTranslate('error', 'First argument to fold is not a function.'));
|
||
}
|
||
if (!Array.isArray(a)) {
|
||
throw new EvalError(qsTranslate('error', 'Second argument to fold is not an array.'));
|
||
}
|
||
return a.reduce(function (acc, x, i) {
|
||
return f(acc, x, i);
|
||
}, init);
|
||
}
|
||
|
||
function arrayFilter(f, a) {
|
||
if (typeof f !== 'function') {
|
||
throw new EvalError(qsTranslate('error', 'First argument to filter is not a function.'));
|
||
}
|
||
if (!Array.isArray(a)) {
|
||
throw new EvalError(qsTranslate('error', 'Second argument to filter is not an array.'));
|
||
}
|
||
return a.filter(function (x, i) {
|
||
return f(x, i);
|
||
});
|
||
}
|
||
|
||
function stringOrArrayIndexOf(target, s) {
|
||
if (!(Array.isArray(s) || typeof s === 'string')) {
|
||
throw new Error(qsTranslate('error', 'Second argument to indexOf is not a string or array.'));
|
||
}
|
||
|
||
return s.indexOf(target);
|
||
}
|
||
|
||
function arrayJoin(sep, a) {
|
||
if (!Array.isArray(a)) {
|
||
throw new Error(qsTranslate('error', 'Second argument to join is not an array.'));
|
||
}
|
||
|
||
return a.join(sep);
|
||
}
|
||
|
||
function sign(x) {
|
||
return ((x > 0) - (x < 0)) || +x;
|
||
}
|
||
|
||
var ONE_THIRD = 1/3;
|
||
function cbrt(x) {
|
||
return x < 0 ? -Math.pow(-x, ONE_THIRD) : Math.pow(x, ONE_THIRD);
|
||
}
|
||
|
||
function expm1(x) {
|
||
return Math.exp(x) - 1;
|
||
}
|
||
|
||
function log1p(x) {
|
||
return Math.log(1 + x);
|
||
}
|
||
|
||
function log2(x) {
|
||
return Math.log(x) / Math.LN2;
|
||
}
|
||
|
||
class Parser {
|
||
constructor(options) {
|
||
this.options = options || {};
|
||
this.unaryOps = {
|
||
sin: Math.sin,
|
||
cos: Math.cos,
|
||
tan: Math.tan,
|
||
asin: Math.asin,
|
||
acos: Math.acos,
|
||
atan: Math.atan,
|
||
sinh: Math.sinh || sinh,
|
||
cosh: Math.cosh || cosh,
|
||
tanh: Math.tanh || tanh,
|
||
asinh: Math.asinh || asinh,
|
||
acosh: Math.acosh || acosh,
|
||
atanh: Math.atanh || atanh,
|
||
sqrt: Math.sqrt,
|
||
cbrt: Math.cbrt || cbrt,
|
||
log: Math.log,
|
||
log2: Math.log2 || log2,
|
||
ln: Math.log,
|
||
lg: Math.log10 || log10,
|
||
log10: Math.log10 || log10,
|
||
expm1: Math.expm1 || expm1,
|
||
log1p: Math.log1p || log1p,
|
||
abs: Math.abs,
|
||
ceil: Math.ceil,
|
||
floor: Math.floor,
|
||
round: Math.round,
|
||
trunc: Math.trunc || trunc,
|
||
'-': neg,
|
||
'+': Number,
|
||
exp: Math.exp,
|
||
not: not,
|
||
length: stringOrArrayLength,
|
||
'!': factorial,
|
||
sign: Math.sign || sign
|
||
};
|
||
|
||
this.binaryOps = {
|
||
'+': add,
|
||
'-': sub,
|
||
'*': mul,
|
||
'/': div,
|
||
'%': mod,
|
||
'^': Math.pow,
|
||
'||': concat,
|
||
'==': equal,
|
||
'!=': notEqual,
|
||
'>': greaterThan,
|
||
'<': lessThan,
|
||
'>=': greaterThanEqual,
|
||
'<=': lessThanEqual,
|
||
and: andOperator,
|
||
or: orOperator,
|
||
'in': inOperator,
|
||
'=': setVar,
|
||
'[': arrayIndex
|
||
};
|
||
|
||
this.ternaryOps = {
|
||
'?': condition
|
||
};
|
||
|
||
this.functions = {
|
||
random: random,
|
||
fac: factorial,
|
||
min: min,
|
||
max: max,
|
||
hypot: Math.hypot || hypot,
|
||
pyt: Math.hypot || hypot, // backward compat
|
||
pow: Math.pow,
|
||
atan2: Math.atan2,
|
||
'if': condition,
|
||
gamma: gamma,
|
||
'Γ': gamma,
|
||
roundTo: roundTo,
|
||
map: arrayMap,
|
||
fold: arrayFold,
|
||
filter: arrayFilter,
|
||
indexOf: stringOrArrayIndexOf,
|
||
join: arrayJoin
|
||
};
|
||
|
||
this.consts = {
|
||
E: Math.E,
|
||
PI: Math.PI,
|
||
'true': true,
|
||
'false': false
|
||
};
|
||
}
|
||
|
||
parse(expr) {
|
||
var instr = [];
|
||
var parserState = new ParserState(
|
||
this,
|
||
new TokenStream(this, expr),
|
||
{ allowMemberAccess: this.options.allowMemberAccess }
|
||
);
|
||
|
||
parserState.parseExpression(instr);
|
||
parserState.expect(TEOF, QT_TRANSLATE_NOOP('error','EOF'));
|
||
|
||
return new Expression(instr, this);
|
||
}
|
||
|
||
evaluate(expr, variables) {
|
||
return this.parse(expr).evaluate(variables);
|
||
}
|
||
};
|
||
|
||
|
||
var sharedParser = new Parser();
|
||
|
||
Parser.parse = function (expr) {
|
||
return sharedParser.parse(expr);
|
||
};
|
||
|
||
Parser.evaluate = function (expr, variables) {
|
||
return sharedParser.parse(expr).evaluate(variables);
|
||
};
|
||
|
||
var optionNameMap = {
|
||
'+': 'add',
|
||
'-': 'subtract',
|
||
'*': 'multiply',
|
||
'/': 'divide',
|
||
'%': 'remainder',
|
||
'^': 'power',
|
||
'!': 'factorial',
|
||
'<': 'comparison',
|
||
'>': 'comparison',
|
||
'<=': 'comparison',
|
||
'>=': 'comparison',
|
||
'==': 'comparison',
|
||
'!=': 'comparison',
|
||
'||': 'concatenate',
|
||
'and': 'logical',
|
||
'or': 'logical',
|
||
'not': 'logical',
|
||
'?': 'conditional',
|
||
':': 'conditional',
|
||
//'=': 'assignment', // Disable assignment
|
||
'[': 'array',
|
||
//'()=': 'fndef' // Diable function definition
|
||
};
|
||
|
||
function getOptionName(op) {
|
||
return optionNameMap.hasOwnProperty(op) ? optionNameMap[op] : op;
|
||
}
|
||
|
||
Parser.prototype.isOperatorEnabled = function (op) {
|
||
var optionName = getOptionName(op);
|
||
var operators = this.options.operators || {};
|
||
|
||
return !(optionName in operators) || !!operators[optionName];
|
||
};
|
||
|
||
/*!
|
||
Based on ndef.parser, by Raphael Graf <r@undefined.ch>
|
||
http://www.undefined.ch/mparser/index.html
|
||
|
||
Ported to JavaScript and modified by Matthew Crumley <email@matthewcrumley.com> (http://silentmatt.com/)
|
||
|
||
Ported to QMLJS with modifications done accordingly done by Ad5001 <mail@ad5001.eu> (https://ad5001.eu)
|
||
|
||
You are free to use and modify this code in anyway you find useful. Please leave this comment in the code
|
||
to acknowledge its original source. If you feel like it, I enjoy hearing about projects that use my code,
|
||
but don't feel like you have to let me know or ask permission.
|
||
*/
|
||
|
||
|