Nearly rewrote expr-eval to be compatible as an ECMAScript module, the last blocker for many bugs and JS tests!
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
commit f91538de446ef0e497ec7e87e2729f504a4172cb Author: Ad5001 <mail@ad5001.eu> Date: Sun Sep 22 05:55:12 2024 +0200 Converted expr-eval back to regular JS! commit 23c346f6c65b5b5c4bb4ad610f0554bd1d9a3700 Author: Ad5001 <mail@ad5001.eu> Date: Sun Sep 22 05:06:59 2024 +0200 Reformatting commit 66608c980fd44f26ae8e6855ecd5fc3e7db55a7b Author: Ad5001 <mail@ad5001.eu> Date: Sun Sep 22 05:04:27 2024 +0200 Removed all 'var's commit 545886fd38c99cf11bc576caa40bec0d7fe0ac30 Author: Ad5001 <mail@ad5001.eu> Date: Sun Sep 22 04:53:32 2024 +0200 Removing function definition commit 489602b24bb70cb6ad782871e269a22c92fcf072 Author: Ad5001 <mail@ad5001.eu> Date: Sun Sep 22 04:51:21 2024 +0200 Removing semicolons commit 3ee069edbeb8ebfb5c7d15d319014f7a085ff623 Author: Ad5001 <mail@ad5001.eu> Date: Sun Sep 22 04:49:32 2024 +0200 Converting all classes to ECMAScript classes
This commit is contained in:
parent
1299fe469d
commit
cc0f277da7
12 changed files with 2340 additions and 2049 deletions
|
@ -23,7 +23,7 @@ import QtQuick.Layouts 1.12
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
|
||||||
// Auto loading all modules.
|
// Auto loading all modules.
|
||||||
import "js/autoload.js" as ModulesAutoload
|
import "js/autoload.mjs" as ModulesAutoload
|
||||||
|
|
||||||
import eu.ad5001.LogarithmPlotter.History 1.0
|
import eu.ad5001.LogarithmPlotter.History 1.0
|
||||||
import eu.ad5001.LogarithmPlotter.ObjectLists 1.0
|
import eu.ad5001.LogarithmPlotter.ObjectLists 1.0
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
|
|
||||||
// Loading modules in order
|
|
||||||
.import "objects.mjs" as Objects
|
|
||||||
.import "lib/expr-eval/integration.js" as ExprParser
|
|
||||||
.import "objs/autoload.mjs" as Autoload
|
|
||||||
.import "math/latex.mjs" as Latex
|
|
||||||
.import "history/common.mjs" as HistoryCommon
|
|
||||||
.import "canvas.mjs" as CanvasAPI
|
|
||||||
.import "io.mjs" as IOAPI
|
|
||||||
.import "preferences.mjs" as PreferencesAPI
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||||
|
* Copyright (C) 2021-2024 Ad5001
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Loading modules in order
|
||||||
|
import * as Objects from "./objects.mjs"
|
||||||
|
import * as ExprParser from "./lib/expr-eval/integration.mjs"
|
||||||
|
import * as ObjsAutoload from "./objs/autoload.mjs"
|
||||||
|
import * as Latex from "./math/latex.mjs"
|
||||||
|
import * as HistoryCommon from "./history/common.mjs"
|
||||||
|
import * as CanvasAPI from "./canvas.mjs"
|
||||||
|
import * as IOAPI from "./io.mjs"
|
||||||
|
import * as PreferencesAPI from "./preferences.mjs"
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,540 @@
|
||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
* https://silentmatt.com/javascript-expression-evaluator/
|
||||||
|
*
|
||||||
|
* Ported to QMLJS with modifications done accordingly done by Ad5001 <mail@ad5001.eu> (https://ad5001.eu)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2015 Matthew Crumley, 2021-2024 Ad5001
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Instruction,
|
||||||
|
IOP3, IOP2, IOP1,
|
||||||
|
INUMBER, IARRAY,
|
||||||
|
IVAR, IVARNAME,
|
||||||
|
IEXPR, IEXPREVAL,
|
||||||
|
IMEMBER, IFUNCALL,
|
||||||
|
IENDSTATEMENT,
|
||||||
|
unaryInstruction, binaryInstruction, ternaryInstruction
|
||||||
|
} from "./instruction.mjs"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simplifies the given instructions
|
||||||
|
* @param {Instruction[]} tokens
|
||||||
|
* @param {Record.<string, function(any): any>} unaryOps
|
||||||
|
* @param {Record.<string, function(any, any): any>} binaryOps
|
||||||
|
* @param {Record.<string, function(any, any, any): any>} ternaryOps
|
||||||
|
* @param {Record.<string, any>} values
|
||||||
|
* @return {Instruction[]}
|
||||||
|
*/
|
||||||
|
function simplify(tokens, unaryOps, binaryOps, ternaryOps, values) {
|
||||||
|
const nstack = []
|
||||||
|
const newexpression = []
|
||||||
|
let n1, n2, n3
|
||||||
|
let f
|
||||||
|
for(let i = 0; i < tokens.length; i++) {
|
||||||
|
let item = tokens[i]
|
||||||
|
const 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()
|
||||||
|
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 {
|
||||||
|
while(nstack.length > 0) {
|
||||||
|
newexpression.push(nstack.shift())
|
||||||
|
}
|
||||||
|
newexpression.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while(nstack.length > 0) {
|
||||||
|
newexpression.push(nstack.shift())
|
||||||
|
}
|
||||||
|
return newexpression
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In the given instructions, replaces variable by expr.
|
||||||
|
* @param {Instruction[]} tokens
|
||||||
|
* @param {string} variable
|
||||||
|
* @param {number} expr
|
||||||
|
* @return {Instruction[]}
|
||||||
|
*/
|
||||||
|
function substitute(tokens, variable, expr) {
|
||||||
|
const newexpression = []
|
||||||
|
for(let i = 0; i < tokens.length; i++) {
|
||||||
|
let item = tokens[i]
|
||||||
|
const type = item.type
|
||||||
|
if(type === IVAR && item.value === variable) {
|
||||||
|
for(let j = 0; j < expr.tokens.length; j++) {
|
||||||
|
const expritem = expr.tokens[j]
|
||||||
|
let 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates the given instructions for a given Expression with given values.
|
||||||
|
* @param {Instruction[]} tokens
|
||||||
|
* @param {Expression} expr
|
||||||
|
* @param {Record.<string, number>} values
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
function evaluate(tokens, expr, values) {
|
||||||
|
const nstack = []
|
||||||
|
let n1, n2, n3
|
||||||
|
let f, args, argCount
|
||||||
|
|
||||||
|
if(isExpressionEvaluator(tokens)) {
|
||||||
|
return resolveExpression(tokens, values)
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let i = 0; i < tokens.length; i++) {
|
||||||
|
const item = tokens[i]
|
||||||
|
const 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 {
|
||||||
|
const 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
|
||||||
|
if(args.length >= 1)
|
||||||
|
nstack.push(f.execute.apply(f, args))
|
||||||
|
else
|
||||||
|
throw new Error(qsTranslate("error", "In order to be executed, object %1 must have at least one argument.").arg(f))
|
||||||
|
} else {
|
||||||
|
throw new Error(qsTranslate("error", "%1 cannot be executed.").arg(f))
|
||||||
|
}
|
||||||
|
} else if(type === IEXPR) {
|
||||||
|
nstack.push(createExpressionEvaluator(item, expr))
|
||||||
|
} else if(type === IEXPREVAL) {
|
||||||
|
nstack.push(item)
|
||||||
|
} else if(type === IMEMBER) {
|
||||||
|
n1 = nstack.pop()
|
||||||
|
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) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the given instructions to a string
|
||||||
|
* If toJS is active, can be evaluated with eval, otherwise it can be reparsed by the parser.
|
||||||
|
* @param {Instruction[]} tokens
|
||||||
|
* @param {boolean} toJS
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
function expressionToString(tokens, toJS) {
|
||||||
|
let nstack = []
|
||||||
|
let n1, n2, n3
|
||||||
|
let f, args, argCount
|
||||||
|
for(let i = 0; i < tokens.length; i++) {
|
||||||
|
const item = tokens[i]
|
||||||
|
const 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 === 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])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function escapeValue(v) {
|
||||||
|
if(typeof v === "string") {
|
||||||
|
return JSON.stringify(v).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pushes all symbols from tokens into the symbols array.
|
||||||
|
* @param {Instruction[]} tokens
|
||||||
|
* @param {string[]} symbols
|
||||||
|
* @param {{withMembers: (boolean|undefined)}}options
|
||||||
|
*/
|
||||||
|
function getSymbols(tokens, symbols, options) {
|
||||||
|
options = options || {}
|
||||||
|
const withMembers = !!options.withMembers
|
||||||
|
let prevVar = null
|
||||||
|
|
||||||
|
for(let i = 0; i < tokens.length; i++) {
|
||||||
|
const item = tokens[i]
|
||||||
|
if(item.type === IVAR || item.type === IVARNAME) {
|
||||||
|
if(!withMembers && !symbols.includes(item.value)) {
|
||||||
|
symbols.push(item.value)
|
||||||
|
} else if(prevVar !== null) {
|
||||||
|
if(!symbols.includes(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(!symbols.includes(prevVar)) {
|
||||||
|
symbols.push(prevVar)
|
||||||
|
}
|
||||||
|
prevVar = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(prevVar !== null && !symbols.includes(prevVar)) {
|
||||||
|
symbols.push(prevVar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Expression {
|
||||||
|
/**
|
||||||
|
* @param {Instruction[]} tokens
|
||||||
|
* @param {Parser} parser
|
||||||
|
*/
|
||||||
|
constructor(tokens, parser) {
|
||||||
|
this.tokens = tokens
|
||||||
|
this.parser = parser
|
||||||
|
this.unaryOps = parser.unaryOps
|
||||||
|
this.binaryOps = parser.binaryOps
|
||||||
|
this.ternaryOps = parser.ternaryOps
|
||||||
|
this.functions = parser.functions
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simplifies the expression.
|
||||||
|
* @param {Object<string, number|Expression>|undefined} values
|
||||||
|
* @returns {Expression}
|
||||||
|
*/
|
||||||
|
simplify(values) {
|
||||||
|
values = values || {}
|
||||||
|
return new Expression(simplify(this.tokens, this.unaryOps, this.binaryOps, this.ternaryOps, values), this.parser)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new expression where the variable is substituted by the given expression.
|
||||||
|
* @param {string} variable
|
||||||
|
* @param {string|Expression} expr
|
||||||
|
* @returns {Expression}
|
||||||
|
*/
|
||||||
|
substitute(variable, expr) {
|
||||||
|
if(!(expr instanceof Expression)) {
|
||||||
|
expr = this.parser.parse(String(expr))
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Expression(substitute(this.tokens, variable, expr), this.parser)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the value of the expression by giving all variables and their corresponding values.
|
||||||
|
* @param {Object<string, number>} values
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
evaluate(values) {
|
||||||
|
values = Object.assign({}, values, this.parser.consts)
|
||||||
|
return evaluate(this.tokens, this, values)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of symbols (string of characters) in the expressions.
|
||||||
|
* Can be functions, constants, or variables.
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
symbols(options) {
|
||||||
|
options = options || {}
|
||||||
|
const vars = []
|
||||||
|
getSymbols(this.tokens, vars, options)
|
||||||
|
return vars
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return expressionToString(this.tokens, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of symbols (string of characters) which are not defined
|
||||||
|
* as constants or functions.
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
variables(options) {
|
||||||
|
options = options || {}
|
||||||
|
const vars = []
|
||||||
|
getSymbols(this.tokens, vars, options)
|
||||||
|
const functions = this.functions
|
||||||
|
const consts = this.parser.consts
|
||||||
|
return vars.filter((name) => {
|
||||||
|
return !(name in functions) && !(name in consts)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the expression to a JS function.
|
||||||
|
* @param {string} param - Parsed variables for the function.
|
||||||
|
* @param {Object.<string, (Expression|string)>} variables - Default variables to provide.
|
||||||
|
* @returns {function(...any)}
|
||||||
|
*/
|
||||||
|
toJSFunction(param, variables) {
|
||||||
|
const expr = this
|
||||||
|
const 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
* https://silentmatt.com/javascript-expression-evaluator/
|
||||||
|
*
|
||||||
|
* Ported to QMLJS with modifications done accordingly done by Ad5001 <mail@ad5001.eu> (https://ad5001.eu)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2015 Matthew Crumley, 2021-2024 Ad5001
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const INUMBER = "INUMBER"
|
||||||
|
export const IOP1 = "IOP1"
|
||||||
|
export const IOP2 = "IOP2"
|
||||||
|
export const IOP3 = "IOP3"
|
||||||
|
export const IVAR = "IVAR"
|
||||||
|
export const IVARNAME = "IVARNAME"
|
||||||
|
export const IFUNCALL = "IFUNCALL"
|
||||||
|
export const IEXPR = "IEXPR"
|
||||||
|
export const IEXPREVAL = "IEXPREVAL"
|
||||||
|
export const IMEMBER = "IMEMBER"
|
||||||
|
export const IENDSTATEMENT = "IENDSTATEMENT"
|
||||||
|
export const IARRAY = "IARRAY"
|
||||||
|
|
||||||
|
|
||||||
|
export class Instruction {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} type
|
||||||
|
* @param {any} value
|
||||||
|
*/
|
||||||
|
constructor(type, value) {
|
||||||
|
this.type = type
|
||||||
|
this.value = (value !== undefined && value !== null) ? value : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
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 IARRAY:
|
||||||
|
return "ARRAY " + this.value
|
||||||
|
case IMEMBER:
|
||||||
|
return "." + this.value
|
||||||
|
default:
|
||||||
|
return "Invalid Instruction"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unaryInstruction(value) {
|
||||||
|
return new Instruction(IOP1, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function binaryInstruction(value) {
|
||||||
|
return new Instruction(IOP2, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ternaryInstruction(value) {
|
||||||
|
return new Instruction(IOP3, value)
|
||||||
|
}
|
|
@ -1,25 +1,23 @@
|
||||||
/**
|
/**
|
||||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||||
* Copyright (C) 2021-2024 Ad5001
|
* Copyright (C) 2021-2024 Ad5001
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful,
|
* This program is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* GNU General Public License for more details.
|
* GNU General Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.pragma library
|
import { Module } from "../../modules.mjs"
|
||||||
|
import { Parser } from "./parser.mjs"
|
||||||
.import "expr-eval.js" as ExprEval
|
|
||||||
.import "../../modules.mjs" as M
|
|
||||||
|
|
||||||
const evalVariables = {
|
const evalVariables = {
|
||||||
// Variables not provided by expr-eval.js, needs to be provided manually
|
// Variables not provided by expr-eval.js, needs to be provided manually
|
||||||
|
@ -36,15 +34,14 @@ const evalVariables = {
|
||||||
"false": false
|
"false": false
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExprParserAPI extends M.Module {
|
export class ExprParserAPI extends Module {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('ExprParser', [
|
super("ExprParser", [
|
||||||
/** @type {ObjectsAPI} */
|
/** @type {ObjectsAPI} */
|
||||||
Modules.Objects
|
Modules.Objects
|
||||||
])
|
])
|
||||||
this.currentVars = {}
|
this.currentVars = {}
|
||||||
this.Internals = ExprEval
|
this._parser = new Parser()
|
||||||
this._parser = new ExprEval.Parser()
|
|
||||||
|
|
||||||
this._parser.consts = Object.assign({}, this._parser.consts, evalVariables)
|
this._parser.consts = Object.assign({}, this._parser.consts, evalVariables)
|
||||||
|
|
||||||
|
@ -65,18 +62,18 @@ class ExprParserAPI extends M.Module {
|
||||||
if(args.length === 1) {
|
if(args.length === 1) {
|
||||||
// Parse object
|
// Parse object
|
||||||
f = args[0]
|
f = args[0]
|
||||||
if(typeof f !== 'object' || !f.execute)
|
if(typeof f !== "object" || !f.execute)
|
||||||
throw EvalError(qsTranslate('usage', 'Usage: %1').arg(usage1))
|
throw EvalError(qsTranslate("usage", "Usage: %1").arg(usage1))
|
||||||
let target = f
|
let target = f
|
||||||
f = (x) => target.execute(x)
|
f = (x) => target.execute(x)
|
||||||
} else if(args.length === 2) {
|
} else if(args.length === 2) {
|
||||||
// Parse variable
|
// Parse variable
|
||||||
[f,variable] = args
|
[f, variable] = args
|
||||||
if(typeof f !== 'string' || typeof variable !== 'string')
|
if(typeof f !== "string" || typeof variable !== "string")
|
||||||
throw EvalError(qsTranslate('usage', 'Usage: %1').arg(usage2))
|
throw EvalError(qsTranslate("usage", "Usage: %1").arg(usage2))
|
||||||
f = this._parser.parse(f).toJSFunction(variable, this.currentVars)
|
f = this._parser.parse(f).toJSFunction(variable, this.currentVars)
|
||||||
} else
|
} else
|
||||||
throw EvalError(qsTranslate('usage', 'Usage: %1 or\n%2').arg(usage1).arg(usage2))
|
throw EvalError(qsTranslate("usage", "Usage: %1 or\n%2").arg(usage1).arg(usage2))
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,27 +85,27 @@ class ExprParserAPI extends M.Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
integral(a, b, ...args) {
|
integral(a, b, ...args) {
|
||||||
let usage1 = qsTranslate('usage', 'integral(<from: number>, <to: number>, <f: ExecutableObject>)')
|
let usage1 = qsTranslate("usage", "integral(<from: number>, <to: number>, <f: ExecutableObject>)")
|
||||||
let usage2 = qsTranslate('usage', 'integral(<from: number>, <to: number>, <f: string>, <variable: string>)')
|
let usage2 = qsTranslate("usage", "integral(<from: number>, <to: number>, <f: string>, <variable: string>)")
|
||||||
let f = this.parseArgumentsForFunction(args, usage1, usage2)
|
let f = this.parseArgumentsForFunction(args, usage1, usage2)
|
||||||
if(a == null || b == null)
|
if(a == null || b == null)
|
||||||
throw EvalError(qsTranslate('usage', 'Usage: %1 or\n%2').arg(usage1).arg(usage2))
|
throw EvalError(qsTranslate("usage", "Usage: %1 or\n%2").arg(usage1).arg(usage2))
|
||||||
|
|
||||||
// https://en.wikipedia.org/wiki/Simpson%27s_rule
|
// https://en.wikipedia.org/wiki/Simpson%27s_rule
|
||||||
// Simpler, faster than tokenizing the expression
|
// Simpler, faster than tokenizing the expression
|
||||||
return (b-a)/6*(f(a)+4*f((a+b)/2)+f(b))
|
return (b - a) / 6 * (f(a) + 4 * f((a + b) / 2) + f(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
derivative(...args) {
|
derivative(...args) {
|
||||||
let usage1 = qsTranslate('usage', 'derivative(<f: ExecutableObject>, <x: number>)')
|
let usage1 = qsTranslate("usage", "derivative(<f: ExecutableObject>, <x: number>)")
|
||||||
let usage2 = qsTranslate('usage', 'derivative(<f: string>, <variable: string>, <x: number>)')
|
let usage2 = qsTranslate("usage", "derivative(<f: string>, <variable: string>, <x: number>)")
|
||||||
let x = args.pop()
|
let x = args.pop()
|
||||||
let f = this.parseArgumentsForFunction(args, usage1, usage2)
|
let f = this.parseArgumentsForFunction(args, usage1, usage2)
|
||||||
if(x == null)
|
if(x == null)
|
||||||
throw EvalError(qsTranslate('usage', 'Usage: %1 or\n%2').arg(usage1).arg(usage2))
|
throw EvalError(qsTranslate("usage", "Usage: %1 or\n%2").arg(usage1).arg(usage2))
|
||||||
|
|
||||||
let derivative_precision = x/10
|
let derivative_precision = x / 10
|
||||||
return (f(x+derivative_precision/2)-f(x-derivative_precision/2))/derivative_precision
|
return (f(x + derivative_precision / 2) - f(x - derivative_precision / 2)) / derivative_precision
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,172 @@
|
||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
* https://silentmatt.com/javascript-expression-evaluator/
|
||||||
|
*
|
||||||
|
* Ported to QMLJS with modifications done accordingly done by Ad5001 <mail@ad5001.eu> (https://ad5001.eu)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2015 Matthew Crumley, 2021-2024 Ad5001
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Polyfill from "./polyfill.mjs"
|
||||||
|
import { ParserState } from "./parserstate.mjs"
|
||||||
|
import { TEOF, TokenStream } from "./tokens.mjs"
|
||||||
|
import { Expression } from "./expression.mjs"
|
||||||
|
|
||||||
|
const 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
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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 || Polyfill.sinh,
|
||||||
|
cosh: Math.cosh || Polyfill.cosh,
|
||||||
|
tanh: Math.tanh || Polyfill.tanh,
|
||||||
|
asinh: Math.asinh || Polyfill.asinh,
|
||||||
|
acosh: Math.acosh || Polyfill.acosh,
|
||||||
|
atanh: Math.atanh || Polyfill.atanh,
|
||||||
|
sqrt: Math.sqrt,
|
||||||
|
cbrt: Math.cbrt || Polyfill.cbrt,
|
||||||
|
log: Math.log,
|
||||||
|
log2: Math.log2 || Polyfill.log2,
|
||||||
|
ln: Math.log,
|
||||||
|
lg: Math.log10 || Polyfill.log10,
|
||||||
|
log10: Math.log10 || Polyfill.log10,
|
||||||
|
expm1: Math.expm1 || Polyfill.expm1,
|
||||||
|
log1p: Math.log1p || Polyfill.log1p,
|
||||||
|
abs: Math.abs,
|
||||||
|
ceil: Math.ceil,
|
||||||
|
floor: Math.floor,
|
||||||
|
round: Math.round,
|
||||||
|
trunc: Math.trunc || Polyfill.trunc,
|
||||||
|
"-": Polyfill.neg,
|
||||||
|
"+": Number,
|
||||||
|
exp: Math.exp,
|
||||||
|
not: Polyfill.not,
|
||||||
|
length: Polyfill.stringOrArrayLength,
|
||||||
|
"!": Polyfill.factorial,
|
||||||
|
sign: Math.sign || Polyfill.sign
|
||||||
|
}
|
||||||
|
this.unaryOpsList = Object.keys(this.unaryOps)
|
||||||
|
|
||||||
|
this.binaryOps = {
|
||||||
|
"+": Polyfill.add,
|
||||||
|
"-": Polyfill.sub,
|
||||||
|
"*": Polyfill.mul,
|
||||||
|
"/": Polyfill.div,
|
||||||
|
"%": Polyfill.mod,
|
||||||
|
"^": Math.pow,
|
||||||
|
"||": Polyfill.concat,
|
||||||
|
"==": Polyfill.equal,
|
||||||
|
"!=": Polyfill.notEqual,
|
||||||
|
">": Polyfill.greaterThan,
|
||||||
|
"<": Polyfill.lessThan,
|
||||||
|
">=": Polyfill.greaterThanEqual,
|
||||||
|
"<=": Polyfill.lessThanEqual,
|
||||||
|
and: Polyfill.andOperator,
|
||||||
|
or: Polyfill.orOperator,
|
||||||
|
"in": Polyfill.inOperator,
|
||||||
|
"=": Polyfill.setVar,
|
||||||
|
"[": Polyfill.arrayIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ternaryOps = {
|
||||||
|
"?": Polyfill.condition
|
||||||
|
}
|
||||||
|
|
||||||
|
this.functions = {
|
||||||
|
random: Polyfill.random,
|
||||||
|
fac: Polyfill.factorial,
|
||||||
|
min: Polyfill.min,
|
||||||
|
max: Polyfill.max,
|
||||||
|
hypot: Math.hypot || Polyfill.hypot,
|
||||||
|
pyt: Math.hypot || Polyfill.hypot, // backward compat
|
||||||
|
pow: Math.pow,
|
||||||
|
atan2: Math.atan2,
|
||||||
|
"if": Polyfill.condition,
|
||||||
|
gamma: Polyfill.gamma,
|
||||||
|
"Γ": Polyfill.gamma,
|
||||||
|
roundTo: Polyfill.roundTo,
|
||||||
|
map: Polyfill.arrayMap,
|
||||||
|
fold: Polyfill.arrayFold,
|
||||||
|
filter: Polyfill.arrayFilter,
|
||||||
|
indexOf: Polyfill.stringOrArrayIndexOf,
|
||||||
|
join: Polyfill.arrayJoin
|
||||||
|
}
|
||||||
|
|
||||||
|
// These constants will automatically be replaced the MOMENT they are parsed.
|
||||||
|
// (Original consts from the parser)
|
||||||
|
this.builtinConsts = {}
|
||||||
|
// These consts will only be replaced when the expression is evaluated.
|
||||||
|
this.consts = {}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(expr) {
|
||||||
|
const instr = []
|
||||||
|
const 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
isOperatorEnabled(op) {
|
||||||
|
const optionName = optionNameMap.hasOwnProperty(op) ? optionNameMap[op] : op
|
||||||
|
const operators = this.options.operators || {}
|
||||||
|
|
||||||
|
return !(optionName in operators) || !!operators[optionName]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,398 @@
|
||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
* https://silentmatt.com/javascript-expression-evaluator/
|
||||||
|
*
|
||||||
|
* Ported to QMLJS with modifications done accordingly done by Ad5001 <mail@ad5001.eu> (https://ad5001.eu)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2015 Matthew Crumley, 2021-2024 Ad5001
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { TBRACKET, TCOMMA, TEOF, TNAME, TNUMBER, TOP, TPAREN, TSTRING } from "./tokens.mjs"
|
||||||
|
import {
|
||||||
|
Instruction,
|
||||||
|
IARRAY, IEXPR, IFUNCALL, IMEMBER,
|
||||||
|
INUMBER, IVAR,
|
||||||
|
ternaryInstruction, binaryInstruction, unaryInstruction
|
||||||
|
} from "./instruction.mjs"
|
||||||
|
|
||||||
|
const COMPARISON_OPERATORS = ["==", "!=", "<", "<=", ">=", ">", "in"]
|
||||||
|
const ADD_SUB_OPERATORS = ["+", "-", "||"]
|
||||||
|
const TERM_OPERATORS = ["*", "/", "%"]
|
||||||
|
|
||||||
|
export class ParserState {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Parser} parser
|
||||||
|
* @param {TokenStream} tokenStream
|
||||||
|
* @param {{[operators]: Object.<string, boolean>, [allowMemberAccess]: boolean}} options
|
||||||
|
*/
|
||||||
|
constructor(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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries the next token for parsing.
|
||||||
|
* @return {Token}
|
||||||
|
*/
|
||||||
|
next() {
|
||||||
|
this.current = this.nextToken
|
||||||
|
this.nextToken = this.tokens.next()
|
||||||
|
return this.nextToken
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given Token matches a condition (called if function, one of if array, and exact match otherwise)
|
||||||
|
* @param {Token} token
|
||||||
|
* @param {Array|function(Token): boolean|string|number|boolean} [value]
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
tokenMatches(token, value) {
|
||||||
|
if(typeof value === "undefined") {
|
||||||
|
return true
|
||||||
|
} else if(Array.isArray(value)) {
|
||||||
|
return value.includes(token.value)
|
||||||
|
} else if(typeof value === "function") {
|
||||||
|
return value(token)
|
||||||
|
} else {
|
||||||
|
return token.value === value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the current state (current and next token) to be restored later.
|
||||||
|
*/
|
||||||
|
save() {
|
||||||
|
this.savedCurrent = this.current
|
||||||
|
this.savedNextToken = this.nextToken
|
||||||
|
this.tokens.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores a previous state (current and next token) from last save.
|
||||||
|
*/
|
||||||
|
restore() {
|
||||||
|
this.tokens.restore()
|
||||||
|
this.current = this.savedCurrent
|
||||||
|
this.nextToken = this.savedNextToken
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the next token matches the given type and value, and if so, consume the current token.
|
||||||
|
* Returns true if the check matches.
|
||||||
|
* @param {string} type
|
||||||
|
* @param {any} [value]
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
accept(type, value) {
|
||||||
|
if(this.nextToken.type === type && this.tokenMatches(this.nextToken, value)) {
|
||||||
|
this.next()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws an error if the next token does not match the given type and value. Otherwise, consumes the current token.
|
||||||
|
* @param {string} type
|
||||||
|
* @param {any} [value]
|
||||||
|
*/
|
||||||
|
expect(type, value) {
|
||||||
|
if(!this.accept(type, value)) {
|
||||||
|
throw new Error(qsTranslate("error", "Parse error [position %1]: %2")
|
||||||
|
.arg(this.tokens.pos)
|
||||||
|
.arg(qsTranslate("error", "Expected %1").arg(value || type)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts enough Tokens to form an expression atom (generally the next part of the expression) into an instruction
|
||||||
|
* and pushes it to the instruction list.
|
||||||
|
* Throws an error if an unexpected token gets parsed.
|
||||||
|
* @param {Instruction[]} instr
|
||||||
|
*/
|
||||||
|
parseAtom(instr) {
|
||||||
|
const prefixOperators = this.tokens.unaryOpsList
|
||||||
|
|
||||||
|
if(this.accept(TNAME) || this.accept(TOP, prefixOperators)) {
|
||||||
|
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 {
|
||||||
|
const argCount = this.parseArrayList(instr)
|
||||||
|
instr.push(new Instruction(IARRAY, argCount))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(qsTranslate("error", "Unexpected %1").arg(this.nextToken))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consumes the next tokens to compile a general expression which should return a value, and compiles
|
||||||
|
* the instructions into the list.
|
||||||
|
* @param {Instruction[]} instr
|
||||||
|
*/
|
||||||
|
parseExpression(instr) {
|
||||||
|
const exprInstr = []
|
||||||
|
this.parseConditionalExpression(exprInstr)
|
||||||
|
instr.push(...exprInstr)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an array indice, and return the number of arguments found at the end.
|
||||||
|
* @param {Instruction[]} instr
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
parseArrayList(instr) {
|
||||||
|
let argCount = 0
|
||||||
|
|
||||||
|
while(!this.accept(TBRACKET, "]")) {
|
||||||
|
this.parseExpression(instr)
|
||||||
|
++argCount
|
||||||
|
while(this.accept(TCOMMA)) {
|
||||||
|
this.parseExpression(instr)
|
||||||
|
++argCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return argCount
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a tertiary statement (<condition> ? <value if true> : <value if false>) and pushes it into the instruction
|
||||||
|
* list.
|
||||||
|
* @param {Instruction[]} instr
|
||||||
|
*/
|
||||||
|
parseConditionalExpression(instr) {
|
||||||
|
this.parseOrExpression(instr)
|
||||||
|
while(this.accept(TOP, "?")) {
|
||||||
|
const trueBranch = []
|
||||||
|
const 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("?"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a binary or statement (<condition 1> or <condition 2>) and pushes it into the instruction list.
|
||||||
|
* @param {Instruction[]} instr
|
||||||
|
*/
|
||||||
|
parseOrExpression(instr) {
|
||||||
|
this.parseAndExpression(instr)
|
||||||
|
while(this.accept(TOP, "or")) {
|
||||||
|
const falseBranch = []
|
||||||
|
this.parseAndExpression(falseBranch)
|
||||||
|
instr.push(new Instruction(IEXPR, falseBranch))
|
||||||
|
instr.push(binaryInstruction("or"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a binary and statement (<condition 1> and <condition 2>) and pushes it into the instruction list.
|
||||||
|
* @param {Instruction[]} instr
|
||||||
|
*/
|
||||||
|
parseAndExpression(instr) {
|
||||||
|
this.parseComparison(instr)
|
||||||
|
while(this.accept(TOP, "and")) {
|
||||||
|
const trueBranch = []
|
||||||
|
this.parseComparison(trueBranch)
|
||||||
|
instr.push(new Instruction(IEXPR, trueBranch))
|
||||||
|
instr.push(binaryInstruction("and"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a binary equality statement (<condition 1> == <condition 2> and so on) and pushes it into the instruction list.
|
||||||
|
* @param {Instruction[]} instr
|
||||||
|
*/
|
||||||
|
parseComparison(instr) {
|
||||||
|
this.parseAddSub(instr)
|
||||||
|
while(this.accept(TOP, COMPARISON_OPERATORS)) {
|
||||||
|
const op = this.current
|
||||||
|
this.parseAddSub(instr)
|
||||||
|
instr.push(binaryInstruction(op.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses add, minus and concat operations and pushes them into the instruction list.
|
||||||
|
* @param {Instruction[]} instr
|
||||||
|
*/
|
||||||
|
parseAddSub(instr) {
|
||||||
|
this.parseTerm(instr)
|
||||||
|
while(this.accept(TOP, ADD_SUB_OPERATORS)) {
|
||||||
|
const op = this.current
|
||||||
|
this.parseTerm(instr)
|
||||||
|
instr.push(binaryInstruction(op.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses times, divide and modulo operations and pushes them into the instruction list.
|
||||||
|
* @param {Instruction[]} instr
|
||||||
|
*/
|
||||||
|
parseTerm(instr) {
|
||||||
|
this.parseFactor(instr)
|
||||||
|
while(this.accept(TOP, TERM_OPERATORS)) {
|
||||||
|
const op = this.current
|
||||||
|
this.parseFactor(instr)
|
||||||
|
instr.push(binaryInstruction(op.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses prefix operations (+, -, but also functions like sin or cos which don't need parentheses)
|
||||||
|
* @param {Instruction[]} instr
|
||||||
|
*/
|
||||||
|
parseFactor(instr) {
|
||||||
|
const prefixOperators = this.tokens.unaryOpsList
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
if(this.accept(TOP, prefixOperators)) {
|
||||||
|
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 === TCOMMA || this.nextToken.type === TEOF || (this.nextToken.type === TPAREN && this.nextToken.value === ")")) {
|
||||||
|
this.restore()
|
||||||
|
this.parseAtom(instr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const op = this.current
|
||||||
|
this.parseFactor(instr)
|
||||||
|
instr.push(unaryInstruction(op.value))
|
||||||
|
} else {
|
||||||
|
this.parseExponential(instr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Instruction[]} instr
|
||||||
|
*/
|
||||||
|
parseExponential(instr) {
|
||||||
|
this.parsePostfixExpression(instr)
|
||||||
|
while(this.accept(TOP, "^")) {
|
||||||
|
this.parseFactor(instr)
|
||||||
|
instr.push(binaryInstruction("^"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses factorial '!' (after the expression to apply it to).
|
||||||
|
* @param {Instruction[]} instr
|
||||||
|
*/
|
||||||
|
parsePostfixExpression(instr) {
|
||||||
|
this.parseFunctionCall(instr)
|
||||||
|
while(this.accept(TOP, "!")) {
|
||||||
|
instr.push(unaryInstruction("!"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a function (name + parentheses + arguments).
|
||||||
|
* @param {Instruction[]} instr
|
||||||
|
*/
|
||||||
|
parseFunctionCall(instr) {
|
||||||
|
const prefixOperators = this.tokens.unaryOpsList
|
||||||
|
|
||||||
|
if(this.accept(TOP, prefixOperators)) {
|
||||||
|
const 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 {
|
||||||
|
const argCount = this.parseArgumentList(instr)
|
||||||
|
instr.push(new Instruction(IFUNCALL, argCount))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a list of arguments, return their quantity.
|
||||||
|
* @param {Instruction[]} instr
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
parseArgumentList(instr) {
|
||||||
|
let argCount = 0
|
||||||
|
|
||||||
|
while(!this.accept(TPAREN, ")")) {
|
||||||
|
this.parseExpression(instr)
|
||||||
|
++argCount
|
||||||
|
while(this.accept(TCOMMA)) {
|
||||||
|
this.parseExpression(instr)
|
||||||
|
++argCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return argCount
|
||||||
|
}
|
||||||
|
|
||||||
|
parseMemberExpression(instr) {
|
||||||
|
this.parseAtom(instr)
|
||||||
|
while(this.accept(TOP, ".") || this.accept(TBRACKET, "[")) {
|
||||||
|
const 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,371 @@
|
||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
* https://silentmatt.com/javascript-expression-evaluator/
|
||||||
|
*
|
||||||
|
* Ported to QMLJS with modifications done accordingly done by Ad5001 <mail@ad5001.eu> (https://ad5001.eu)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2015 Matthew Crumley, 2021-2024 Ad5001
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function add(a, b) {
|
||||||
|
return Number(a) + Number(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sub(a, b) {
|
||||||
|
return a - b
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mul(a, b) {
|
||||||
|
return a * b
|
||||||
|
}
|
||||||
|
|
||||||
|
export function div(a, b) {
|
||||||
|
return a / b
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mod(a, b) {
|
||||||
|
return a % b
|
||||||
|
}
|
||||||
|
|
||||||
|
export function concat(a, b) {
|
||||||
|
if(Array.isArray(a) && Array.isArray(b)) {
|
||||||
|
return a.concat(b)
|
||||||
|
}
|
||||||
|
return "" + a + b
|
||||||
|
}
|
||||||
|
|
||||||
|
export function equal(a, b) {
|
||||||
|
return a === b
|
||||||
|
}
|
||||||
|
|
||||||
|
export function notEqual(a, b) {
|
||||||
|
return a !== b
|
||||||
|
}
|
||||||
|
|
||||||
|
export function greaterThan(a, b) {
|
||||||
|
return a > b
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lessThan(a, b) {
|
||||||
|
return a < b
|
||||||
|
}
|
||||||
|
|
||||||
|
export function greaterThanEqual(a, b) {
|
||||||
|
return a >= b
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lessThanEqual(a, b) {
|
||||||
|
return a <= b
|
||||||
|
}
|
||||||
|
|
||||||
|
export function andOperator(a, b) {
|
||||||
|
return Boolean(a && b)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function orOperator(a, b) {
|
||||||
|
return Boolean(a || b)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inOperator(a, b) {
|
||||||
|
return b.includes(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sinh(a) {
|
||||||
|
return ((Math.exp(a) - Math.exp(-a)) / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cosh(a) {
|
||||||
|
return ((Math.exp(a) + Math.exp(-a)) / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function asinh(a) {
|
||||||
|
if(a === -Infinity) return a
|
||||||
|
return Math.log(a + Math.sqrt((a * a) + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function acosh(a) {
|
||||||
|
return Math.log(a + Math.sqrt((a * a) - 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function atanh(a) {
|
||||||
|
return (Math.log((1 + a) / (1 - a)) / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function log10(a) {
|
||||||
|
return Math.log(a) * Math.LOG10E
|
||||||
|
}
|
||||||
|
|
||||||
|
export function neg(a) {
|
||||||
|
return -a
|
||||||
|
}
|
||||||
|
|
||||||
|
export function not(a) {
|
||||||
|
return !a
|
||||||
|
}
|
||||||
|
|
||||||
|
export function trunc(a) {
|
||||||
|
return a < 0 ? Math.ceil(a) : Math.floor(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function random(a) {
|
||||||
|
return Math.random() * (a || 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function factorial(a) { // a!
|
||||||
|
return gamma(a + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isInteger(value) {
|
||||||
|
return isFinite(value) && (value === Math.round(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
const GAMMA_G = 4.7421875
|
||||||
|
const 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
|
||||||
|
export function gamma(n) {
|
||||||
|
let t, x
|
||||||
|
|
||||||
|
if(isInteger(n)) {
|
||||||
|
if(n <= 0) {
|
||||||
|
return isFinite(n) ? Infinity : NaN
|
||||||
|
}
|
||||||
|
|
||||||
|
if(n > 171) {
|
||||||
|
return Infinity // Will overflow
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = n - 2
|
||||||
|
let 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
|
||||||
|
const twoN = n * n
|
||||||
|
const threeN = twoN * n
|
||||||
|
const fourN = threeN * n
|
||||||
|
const 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(let 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
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stringOrArrayLength(s) {
|
||||||
|
if(Array.isArray(s)) {
|
||||||
|
return s.length
|
||||||
|
}
|
||||||
|
return String(s).length
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hypot() {
|
||||||
|
let sum = 0
|
||||||
|
let larg = 0
|
||||||
|
for(let i = 0; i < arguments.length; i++) {
|
||||||
|
const arg = Math.abs(arguments[i])
|
||||||
|
let 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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.
|
||||||
|
*/
|
||||||
|
export 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setVar(name, value, variables) {
|
||||||
|
if(variables) variables[name] = value
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
export function arrayIndex(array, index) {
|
||||||
|
return array[index | 0]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function max(array) {
|
||||||
|
if(arguments.length === 1 && Array.isArray(array)) {
|
||||||
|
return Math.max.apply(Math, array)
|
||||||
|
} else if(arguments.length >= 1) {
|
||||||
|
return Math.max.apply(Math, arguments)
|
||||||
|
} else {
|
||||||
|
throw new EvalError(qsTranslate("error", "Function %1 must have at least one argument.").arg("max"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function min(array) {
|
||||||
|
if(arguments.length === 1 && Array.isArray(array)) {
|
||||||
|
return Math.min.apply(Math, array)
|
||||||
|
} else if(arguments.length >= 1) {
|
||||||
|
return Math.min.apply(Math, arguments)
|
||||||
|
} else {
|
||||||
|
throw new EvalError(qsTranslate("error", "Function %1 must have at least one argument.").arg("min"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sign(x) {
|
||||||
|
return ((x > 0) - (x < 0)) || +x
|
||||||
|
}
|
||||||
|
|
||||||
|
const ONE_THIRD = 1 / 3
|
||||||
|
|
||||||
|
export function cbrt(x) {
|
||||||
|
return x < 0 ? -Math.pow(-x, ONE_THIRD) : Math.pow(x, ONE_THIRD)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expm1(x) {
|
||||||
|
return Math.exp(x) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
export function log1p(x) {
|
||||||
|
return Math.log(1 + x)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function log2(x) {
|
||||||
|
return Math.log(x) / Math.LN2
|
||||||
|
}
|
|
@ -0,0 +1,575 @@
|
||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
* https://silentmatt.com/javascript-expression-evaluator/
|
||||||
|
*
|
||||||
|
* Ported to QMLJS with modifications done accordingly done by Ad5001 <mail@ad5001.eu> (https://ad5001.eu)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2015 Matthew Crumley, 2021-2024 Ad5001
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const TEOF = "TEOF"
|
||||||
|
export const TOP = "TOP"
|
||||||
|
export const TNUMBER = "TNUMBER"
|
||||||
|
export const TSTRING = "TSTRING"
|
||||||
|
export const TPAREN = "TPAREN"
|
||||||
|
export const TBRACKET = "TBRACKET"
|
||||||
|
export const TCOMMA = "TCOMMA"
|
||||||
|
export const TNAME = "TNAME"
|
||||||
|
|
||||||
|
|
||||||
|
// Additional variable characters.
|
||||||
|
export const ADDITIONAL_VARCHARS = [
|
||||||
|
"α", "β", "γ", "δ", "ε", "ζ", "η",
|
||||||
|
"π", "θ", "κ", "λ", "μ", "ξ", "ρ",
|
||||||
|
"ς", "σ", "τ", "φ", "χ", "ψ", "ω",
|
||||||
|
"Γ", "Δ", "Θ", "Λ", "Ξ", "Π", "Σ",
|
||||||
|
"Φ", "Ψ", "Ω", "ₐ", "ₑ", "ₒ", "ₓ",
|
||||||
|
"ₕ", "ₖ", "ₗ", "ₘ", "ₙ", "ₚ", "ₛ",
|
||||||
|
"ₜ", "¹", "²", "³", "⁴", "⁵", "⁶",
|
||||||
|
"⁷", "⁸", "⁹", "⁰", "₁", "₂", "₃",
|
||||||
|
"₄", "₅", "₆", "₇", "₈", "₉", "₀",
|
||||||
|
"∞", "π"
|
||||||
|
]
|
||||||
|
|
||||||
|
export class Token {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} type - Type of the token (see above).
|
||||||
|
* @param {any} value - Value of the token.
|
||||||
|
* @param {number} index - Index in the string of the token.
|
||||||
|
*/
|
||||||
|
constructor(type, value, index) {
|
||||||
|
this.type = type
|
||||||
|
this.value = value
|
||||||
|
this.index = index
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return this.type + ": " + this.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const unicodeCodePointPattern = /^[0-9a-f]{4}$/i
|
||||||
|
|
||||||
|
export class TokenStream {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Parser} parser
|
||||||
|
* @param {string} expression
|
||||||
|
*/
|
||||||
|
constructor(parser, expression) {
|
||||||
|
this.pos = 0
|
||||||
|
this.current = null
|
||||||
|
this.unaryOps = parser.unaryOps
|
||||||
|
this.unaryOpsList = parser.unaryOpsList
|
||||||
|
this.binaryOps = parser.binaryOps
|
||||||
|
this.ternaryOps = parser.ternaryOps
|
||||||
|
this.builtinConsts = parser.builtinConsts
|
||||||
|
this.expression = expression
|
||||||
|
this.savedPosition = 0
|
||||||
|
this.savedCurrent = null
|
||||||
|
this.options = parser.options
|
||||||
|
this.parser = parser
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} type - Type of the token (see above).
|
||||||
|
* @param {any} value - Value of the token.
|
||||||
|
* @param {number} [pos] - Index in the string of the token.
|
||||||
|
*/
|
||||||
|
newToken(type, value, pos) {
|
||||||
|
return new Token(type, value, pos != null ? pos : this.pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the current position and token into the object.
|
||||||
|
*/
|
||||||
|
save() {
|
||||||
|
this.savedPosition = this.pos
|
||||||
|
this.savedCurrent = this.current
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restored the saved position and token into the current.
|
||||||
|
*/
|
||||||
|
restore() {
|
||||||
|
this.pos = this.savedPosition
|
||||||
|
this.current = this.savedCurrent
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consumes the character at the current position and advance it
|
||||||
|
* until it makes a valid token, and returns it.
|
||||||
|
* @returns {Token}
|
||||||
|
*/
|
||||||
|
next() {
|
||||||
|
if(this.pos >= this.expression.length) {
|
||||||
|
return this.newToken(TEOF, "EOF")
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.isWhitespace()) {
|
||||||
|
return this.next()
|
||||||
|
} else if(this.isRadixInteger() ||
|
||||||
|
this.isNumber() ||
|
||||||
|
this.isOperator() ||
|
||||||
|
this.isString() ||
|
||||||
|
this.isParen() ||
|
||||||
|
this.isBracket() ||
|
||||||
|
this.isComma() ||
|
||||||
|
this.isNamedOp() ||
|
||||||
|
this.isConst() ||
|
||||||
|
this.isName()) {
|
||||||
|
return this.current
|
||||||
|
} else {
|
||||||
|
this.parseError(qsTranslate("error", "Unknown character \"%1\".").arg(this.expression.charAt(this.pos)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the character at the current position starts a string, and if so, consumes it as the current token
|
||||||
|
* and returns true. Otherwise, returns false.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isString() {
|
||||||
|
const startPos = this.pos
|
||||||
|
const quote = this.expression.charAt(startPos)
|
||||||
|
let r = false
|
||||||
|
|
||||||
|
if(quote === "'" || quote === "\"") {
|
||||||
|
let 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) !== "\\") {
|
||||||
|
const 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the character at the current pos is a parenthesis, and if so consumes it into current
|
||||||
|
* and returns true. Otherwise, returns false.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isParen() {
|
||||||
|
const c = this.expression.charAt(this.pos)
|
||||||
|
if(c === "(" || c === ")") {
|
||||||
|
this.current = this.newToken(TPAREN, c)
|
||||||
|
this.pos++
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the character at the current pos is a bracket, and if so consumes it into current
|
||||||
|
* and returns true. Otherwise, returns false.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isBracket() {
|
||||||
|
const c = this.expression.charAt(this.pos)
|
||||||
|
if((c === "[" || c === "]") && this.isOperatorEnabled("[")) {
|
||||||
|
this.current = this.newToken(TBRACKET, c)
|
||||||
|
this.pos++
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the character at the current pos is a comma, and if so consumes it into current
|
||||||
|
* and returns true. Otherwise, returns false.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isComma() {
|
||||||
|
const c = this.expression.charAt(this.pos)
|
||||||
|
if(c === ",") {
|
||||||
|
this.current = this.newToken(TCOMMA, ",")
|
||||||
|
this.pos++
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current character is an identifier and makes a const, and if so, consumes it as the current token
|
||||||
|
* and returns true. Otherwise, returns false.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isConst() {
|
||||||
|
const startPos = this.pos
|
||||||
|
let i = startPos
|
||||||
|
for(; i < this.expression.length; i++) {
|
||||||
|
const c = this.expression.charAt(i)
|
||||||
|
if(c.toUpperCase() === c.toLowerCase() && !ADDITIONAL_VARCHARS.includes(c)) {
|
||||||
|
if(i === this.pos || (c !== "_" && c !== "." && (c < "0" || c > "9"))) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(i > startPos) {
|
||||||
|
const str = this.expression.substring(startPos, i)
|
||||||
|
if(str in this.builtinConsts) {
|
||||||
|
this.current = this.newToken(TNUMBER, this.builtinConsts[str])
|
||||||
|
this.pos += str.length
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current character is an identifier and makes a function or an operator, and if so, consumes it as the current token
|
||||||
|
* and returns true. Otherwise, returns false.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isNamedOp() {
|
||||||
|
const startPos = this.pos
|
||||||
|
let i = startPos
|
||||||
|
for(; i < this.expression.length; i++) {
|
||||||
|
const c = this.expression.charAt(i)
|
||||||
|
if(c.toUpperCase() === c.toLowerCase()) {
|
||||||
|
if(i === this.pos || (c !== "_" && (c < "0" || c > "9"))) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(i > startPos) {
|
||||||
|
const 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current character is an identifier and makes a variable, and if so, consumes it as the current token
|
||||||
|
* and returns true. Otherwise, returns false.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isName() {
|
||||||
|
const startPos = this.pos
|
||||||
|
let i = startPos
|
||||||
|
let hasLetter = false
|
||||||
|
for(; i < this.expression.length; i++) {
|
||||||
|
const c = this.expression.charAt(i)
|
||||||
|
if(c.toUpperCase() === c.toLowerCase() && !ADDITIONAL_VARCHARS.includes(c)) {
|
||||||
|
if(i === this.pos && (c === "$" || c === "_")) {
|
||||||
|
if(c === "_") {
|
||||||
|
hasLetter = true
|
||||||
|
}
|
||||||
|
} else if(i === this.pos || !hasLetter || (c !== "_" && (c < "0" || c > "9"))) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hasLetter = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(hasLetter) {
|
||||||
|
const str = this.expression.substring(startPos, i)
|
||||||
|
this.current = this.newToken(TNAME, str)
|
||||||
|
this.pos += str.length
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the character at the current position is a whitespace, and if so, consumes all consecutive whitespaces
|
||||||
|
* and returns true. Otherwise, returns false.
|
||||||
|
* @returns {boolean}
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
isWhitespace() {
|
||||||
|
let r = false
|
||||||
|
let 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current character is a zero, and checks whether it forms a radix number, and if so, consumes it as the current token
|
||||||
|
* and returns true. Otherwise, returns false.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isRadixInteger() {
|
||||||
|
let pos = this.pos
|
||||||
|
|
||||||
|
if(pos >= this.expression.length - 2 || this.expression.charAt(pos) !== "0") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
++pos
|
||||||
|
|
||||||
|
let radix
|
||||||
|
let 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
|
||||||
|
}
|
||||||
|
|
||||||
|
let valid = false
|
||||||
|
const startPos = pos
|
||||||
|
|
||||||
|
while(pos < this.expression.length) {
|
||||||
|
const 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current character is a digit, and checks whether it forms a number, and if so, consumes it as the current token
|
||||||
|
* and returns true. Otherwise, returns false.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isNumber() {
|
||||||
|
const startPos = this.pos
|
||||||
|
let valid = false
|
||||||
|
let pos = startPos
|
||||||
|
let resetPos = startPos
|
||||||
|
let foundDot = false
|
||||||
|
let foundDigits = false
|
||||||
|
let c
|
||||||
|
|
||||||
|
// Check for digit with dot.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for e exponents.
|
||||||
|
if(c === "e" || c === "E") {
|
||||||
|
pos++
|
||||||
|
let acceptSign = true
|
||||||
|
let 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use parseFloat now that we've identified the number.
|
||||||
|
if(valid) {
|
||||||
|
this.current = this.newToken(TNUMBER, parseFloat(this.expression.substring(startPos, pos)))
|
||||||
|
this.pos = pos
|
||||||
|
} else {
|
||||||
|
this.pos = resetPos
|
||||||
|
}
|
||||||
|
return valid
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current character is an operator, checks whether it's enabled and if so, consumes it as the current token
|
||||||
|
* and returns true. Otherwise, returns false.
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
isOperator() {
|
||||||
|
const startPos = this.pos
|
||||||
|
const 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces a backslash and a character by its unescaped value.
|
||||||
|
* @param {string} v - string to un escape.
|
||||||
|
*/
|
||||||
|
unescape(v) {
|
||||||
|
let index = v.indexOf("\\")
|
||||||
|
if(index < 0) {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
let buffer = v.substring(0, index)
|
||||||
|
while(index >= 0) {
|
||||||
|
const 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
|
||||||
|
const codePoint = v.substring(index + 1, index + 5)
|
||||||
|
if(!unicodeCodePointPattern.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
|
||||||
|
const backslash = v.indexOf("\\", index)
|
||||||
|
buffer += v.substring(index, backslash < 0 ? v.length : backslash)
|
||||||
|
index = backslash
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand for the parser's method to check if an operator is enabled.
|
||||||
|
* @param {string} op
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
isOperatorEnabled(op) {
|
||||||
|
return this.parser.isOperatorEnabled(op)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws a translated error.
|
||||||
|
* @param {string} msg
|
||||||
|
*/
|
||||||
|
parseError(msg) {
|
||||||
|
throw new Error(qsTranslate("error", "Parse error [position %1]: %2").arg(this.pos).arg(msg))
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,50 +1,52 @@
|
||||||
/**
|
/**
|
||||||
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
||||||
* Copyright (C) 2021-2024 Ad5001
|
* Copyright (C) 2021-2024 Ad5001
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful,
|
* This program is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* GNU General Public License for more details.
|
* GNU General Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Module } from '../modules.mjs'
|
import { Module } from "../modules.mjs"
|
||||||
|
import * as Instruction from "../lib/expr-eval/instruction.mjs"
|
||||||
|
import { escapeValue } from "../lib/expr-eval/expression.mjs"
|
||||||
|
|
||||||
const unicodechars = ["α","β","γ","δ","ε","ζ","η",
|
const unicodechars = ["α", "β", "γ", "δ", "ε", "ζ", "η",
|
||||||
"π","θ","κ","λ","μ","ξ","ρ",
|
"π", "θ", "κ", "λ", "μ", "ξ", "ρ",
|
||||||
"ς","σ","τ","φ","χ","ψ","ω",
|
"ς", "σ", "τ", "φ", "χ", "ψ", "ω",
|
||||||
"Γ","Δ","Θ","Λ","Ξ","Π","Σ",
|
"Γ", "Δ", "Θ", "Λ", "Ξ", "Π", "Σ",
|
||||||
"Φ","Ψ","Ω","ₐ","ₑ","ₒ","ₓ",
|
"Φ", "Ψ", "Ω", "ₐ", "ₑ", "ₒ", "ₓ",
|
||||||
"ₕ","ₖ","ₗ","ₘ","ₙ","ₚ","ₛ",
|
"ₕ", "ₖ", "ₗ", "ₘ", "ₙ", "ₚ", "ₛ",
|
||||||
"ₜ","¹","²","³","⁴","⁵","⁶",
|
"ₜ", "¹", "²", "³", "⁴", "⁵", "⁶",
|
||||||
"⁷","⁸","⁹","⁰","₁","₂","₃",
|
"⁷", "⁸", "⁹", "⁰", "₁", "₂", "₃",
|
||||||
"₄","₅","₆","₇","₈","₉","₀",
|
"₄", "₅", "₆", "₇", "₈", "₉", "₀",
|
||||||
"pi", "∞"]
|
"pi", "∞"]
|
||||||
const equivalchars = ["\\alpha","\\beta","\\gamma","\\delta","\\epsilon","\\zeta","\\eta",
|
const equivalchars = ["\\alpha", "\\beta", "\\gamma", "\\delta", "\\epsilon", "\\zeta", "\\eta",
|
||||||
"\\pi","\\theta","\\kappa","\\lambda","\\mu","\\xi","\\rho",
|
"\\pi", "\\theta", "\\kappa", "\\lambda", "\\mu", "\\xi", "\\rho",
|
||||||
"\\sigma","\\sigma","\\tau","\\phi","\\chi","\\psi","\\omega",
|
"\\sigma", "\\sigma", "\\tau", "\\phi", "\\chi", "\\psi", "\\omega",
|
||||||
"\\Gamma","\\Delta","\\Theta","\\Lambda","\\Xi","\\Pi","\\Sigma",
|
"\\Gamma", "\\Delta", "\\Theta", "\\Lambda", "\\Xi", "\\Pi", "\\Sigma",
|
||||||
"\\Phy","\\Psi","\\Omega","{}_{a}","{}_{e}","{}_{o}","{}_{x}",
|
"\\Phy", "\\Psi", "\\Omega", "{}_{a}", "{}_{e}", "{}_{o}", "{}_{x}",
|
||||||
"{}_{h}","{}_{k}","{}_{l}","{}_{m}","{}_{n}","{}_{p}","{}_{s}",
|
"{}_{h}", "{}_{k}", "{}_{l}", "{}_{m}", "{}_{n}", "{}_{p}", "{}_{s}",
|
||||||
"{}_{t}","{}^{1}","{}^{2}","{}^{3}","{}^{4}","{}^{5}","{}^{6}",
|
"{}_{t}", "{}^{1}", "{}^{2}", "{}^{3}", "{}^{4}", "{}^{5}", "{}^{6}",
|
||||||
"{}^{7}","{}^{8}","{}^{9}","{}^{0}","{}_{1}","{}_{2}","{}_{3}",
|
"{}^{7}", "{}^{8}", "{}^{9}", "{}^{0}", "{}_{1}", "{}_{2}", "{}_{3}",
|
||||||
"{}_{4}","{}_{5}","{}_{6}","{}_{7}","{}_{8}","{}_{9}","{}_{0}",
|
"{}_{4}", "{}_{5}", "{}_{6}", "{}_{7}", "{}_{8}", "{}_{9}", "{}_{0}",
|
||||||
"\\pi", "\\infty"]
|
"\\pi", "\\infty"]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class containing the result of a LaTeX render.
|
* Class containing the result of a LaTeX render.
|
||||||
*
|
*
|
||||||
* @property {string} source - Exported PNG file
|
* @property {string} source - Exported PNG file
|
||||||
* @property {number} width
|
* @property {number} width
|
||||||
* @property {number} height
|
* @property {number} height
|
||||||
*/
|
*/
|
||||||
class LatexRenderResult {
|
class LatexRenderResult {
|
||||||
constructor(source, width, height) {
|
constructor(source, width, height) {
|
||||||
|
@ -56,7 +58,7 @@ class LatexRenderResult {
|
||||||
|
|
||||||
class LatexAPI extends Module {
|
class LatexAPI extends Module {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('Latex', [
|
super("Latex", [
|
||||||
/** @type {ExprParserAPI} */
|
/** @type {ExprParserAPI} */
|
||||||
Modules.ExprParser
|
Modules.ExprParser
|
||||||
])
|
])
|
||||||
|
@ -65,10 +67,10 @@ class LatexAPI extends Module {
|
||||||
*/
|
*/
|
||||||
this.enabled = Helper.getSettingBool("enable_latex")
|
this.enabled = Helper.getSettingBool("enable_latex")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepares and renders a latex string into a png file.
|
* Prepares and renders a latex string into a png file.
|
||||||
*
|
*
|
||||||
* @param {string} markup - LaTeX markup to render.
|
* @param {string} markup - LaTeX markup to render.
|
||||||
* @param {number} fontSize - Font size (in pt) to render.
|
* @param {number} fontSize - Font size (in pt) to render.
|
||||||
* @param {color} color - Color of the text to render.
|
* @param {color} color - Color of the text to render.
|
||||||
|
@ -78,11 +80,11 @@ class LatexAPI extends Module {
|
||||||
let args = Latex.render(markup, fontSize, color).split(",")
|
let args = Latex.render(markup, fontSize, color).split(",")
|
||||||
return new LatexRenderResult(...args)
|
return new LatexRenderResult(...args)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the given markup (with given font size and color) has already been
|
* Checks if the given markup (with given font size and color) has already been
|
||||||
* rendered, and if so, returns its data. Otherwise, returns null.
|
* rendered, and if so, returns its data. Otherwise, returns null.
|
||||||
*
|
*
|
||||||
* @param {string} markup - LaTeX markup to render.
|
* @param {string} markup - LaTeX markup to render.
|
||||||
* @param {number} fontSize - Font size (in pt) to render.
|
* @param {number} fontSize - Font size (in pt) to render.
|
||||||
* @param {color} color - Color of the text to render.
|
* @param {color} color - Color of the text to render.
|
||||||
|
@ -95,14 +97,14 @@ class LatexAPI extends Module {
|
||||||
ret = new LatexRenderResult(...data.split(","))
|
ret = new LatexRenderResult(...data.split(","))
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepares and renders a latex string into a png file asynchronously.
|
* Prepares and renders a latex string into a png file asynchronously.
|
||||||
*
|
*
|
||||||
* @param {string} markup - LaTeX markup to render.
|
* @param {string} markup - LaTeX markup to render.
|
||||||
* @param {number} fontSize - Font size (in pt) to render.
|
* @param {number} fontSize - Font size (in pt) to render.
|
||||||
* @param {color} color - Color of the text to render.
|
* @param {color} color - Color of the text to render.
|
||||||
* @returns {Promize<LatexRenderResult>}
|
* @returns {Promise<LatexRenderResult>}
|
||||||
*/
|
*/
|
||||||
requestAsyncRender(markup, fontSize, color) {
|
requestAsyncRender(markup, fontSize, color) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
|
@ -113,11 +115,11 @@ class LatexAPI extends Module {
|
||||||
/**
|
/**
|
||||||
* Puts element within parenthesis.
|
* Puts element within parenthesis.
|
||||||
*
|
*
|
||||||
* @param {string} elem - element to put within parenthesis.
|
* @param {string|number} elem - element to put within parenthesis.
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
par(elem) {
|
par(elem) {
|
||||||
return '(' + elem + ')'
|
return `(${elem})`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -125,16 +127,16 @@ class LatexAPI extends Module {
|
||||||
* the string array contents, but not at the first position of the string,
|
* the string array contents, but not at the first position of the string,
|
||||||
* and returns the parenthesis version if so.
|
* and returns the parenthesis version if so.
|
||||||
*
|
*
|
||||||
* @param {string} elem - element to put within parenthesis.
|
* @param {string|number} elem - element to put within parenthesis.
|
||||||
* @param {Array} contents - Array of elements to put within parenthesis.
|
* @param {Array} contents - Array of elements to put within parenthesis.
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
parif(elem, contents) {
|
parif(elem, contents) {
|
||||||
elem = elem.toString()
|
elem = elem.toString()
|
||||||
if(elem[0] !== "(" && elem[elem.length-1] !== ")" && contents.some(x => elem.indexOf(x) > 0))
|
if(elem[0] !== "(" && elem[elem.length - 1] !== ")" && contents.some(x => elem.indexOf(x) > 0))
|
||||||
return this.par(elem)
|
return this.par(elem)
|
||||||
if(elem[0] === "(" && elem[elem.length-1] === ")")
|
if(elem[0] === "(" && elem[elem.length - 1] === ")")
|
||||||
return elem.substr(1, elem.length-2)
|
return elem.substr(1, elem.length - 2)
|
||||||
return elem
|
return elem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,31 +151,24 @@ class LatexAPI extends Module {
|
||||||
switch(f) {
|
switch(f) {
|
||||||
case "derivative":
|
case "derivative":
|
||||||
if(args.length === 3)
|
if(args.length === 3)
|
||||||
return '\\frac{d' + args[0].substr(1, args[0].length-2).replace(new RegExp(args[1].substr(1, args[1].length-2), 'g'), 'x') + '}{dx}';
|
return "\\frac{d" + args[0].substr(1, args[0].length - 2).replace(new RegExp(args[1].substr(1, args[1].length - 2), "g"), "x") + "}{dx}"
|
||||||
else
|
else
|
||||||
return '\\frac{d' + args[0] + '}{dx}(x)';
|
return "\\frac{d" + args[0] + "}{dx}(x)"
|
||||||
break;
|
|
||||||
case "integral":
|
case "integral":
|
||||||
if(args.length === 4)
|
if(args.length === 4)
|
||||||
return '\\int\\limits_{' + args[0] + '}^{' + args[1] + '}' + args[2].substr(1, args[2].length-2) + ' d' + args[3].substr(1, args[3].length-2);
|
return "\\int\\limits_{" + args[0] + "}^{" + args[1] + "}" + args[2].substr(1, args[2].length - 2) + " d" + args[3].substr(1, args[3].length - 2)
|
||||||
else
|
else
|
||||||
return '\\int\\limits_{' + args[0] + '}^{' + args[1] + '}' + args[2] + '(t) dt';
|
return "\\int\\limits_{" + args[0] + "}^{" + args[1] + "}" + args[2] + "(t) dt"
|
||||||
break;
|
|
||||||
case "sqrt":
|
case "sqrt":
|
||||||
return '\\sqrt\\left(' + args.join(', ') + '\\right)';
|
return "\\sqrt\\left(" + args.join(", ") + "\\right)"
|
||||||
break;
|
|
||||||
case "abs":
|
case "abs":
|
||||||
return '\\left|' + args.join(', ') + '\\right|';
|
return "\\left|" + args.join(", ") + "\\right|"
|
||||||
break;
|
|
||||||
case "floor":
|
case "floor":
|
||||||
return '\\left\\lfloor' + args.join(', ') + '\\right\\rfloor';
|
return "\\left\\lfloor" + args.join(", ") + "\\right\\rfloor"
|
||||||
break;
|
|
||||||
case "ceil":
|
case "ceil":
|
||||||
return '\\left\\lceil' + args.join(', ') + '\\right\\rceil';
|
return "\\left\\lceil" + args.join(", ") + "\\right\\rceil"
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
return '\\mathrm{' + f + '}\\left(' + args.join(', ') + '\\right)';
|
return "\\mathrm{" + f + "}\\left(" + args.join(", ") + "\\right)"
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,150 +183,146 @@ class LatexAPI extends Module {
|
||||||
if(wrapIn$)
|
if(wrapIn$)
|
||||||
for(let i = 0; i < unicodechars.length; i++) {
|
for(let i = 0; i < unicodechars.length; i++) {
|
||||||
if(vari.includes(unicodechars[i]))
|
if(vari.includes(unicodechars[i]))
|
||||||
vari = vari.replace(new RegExp(unicodechars[i], 'g'), '$'+equivalchars[i]+'$')
|
vari = vari.replace(new RegExp(unicodechars[i], "g"), "$" + equivalchars[i] + "$")
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
for(let i = 0; i < unicodechars.length; i++) {
|
for(let i = 0; i < unicodechars.length; i++) {
|
||||||
if(vari.includes(unicodechars[i]))
|
if(vari.includes(unicodechars[i]))
|
||||||
vari = vari.replace(new RegExp(unicodechars[i], 'g'), equivalchars[i])
|
vari = vari.replace(new RegExp(unicodechars[i], "g"), equivalchars[i])
|
||||||
}
|
}
|
||||||
return vari;
|
return vari
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts expr-eval tokens to a latex string.
|
* Converts expr-eval instructions to a latex string.
|
||||||
*
|
*
|
||||||
* @param {Array} tokens - expr-eval tokens list
|
* @param {Instruction[]} instructions - expr-eval tokens list
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
expression(tokens) {
|
expression(instructions) {
|
||||||
let nstack = []
|
let nstack = []
|
||||||
let n1, n2, n3
|
let n1, n2, n3
|
||||||
let f, args, argCount
|
let f, args, argCount
|
||||||
for (let i = 0; i < tokens.length; i++) {
|
for(let item of instructions) {
|
||||||
let item = tokens[i]
|
|
||||||
let type = item.type
|
let type = item.type
|
||||||
|
|
||||||
switch(type) {
|
switch(type) {
|
||||||
case Modules.ExprParser.Internals.INUMBER:
|
case Instruction.INUMBER:
|
||||||
if(item.value === Infinity) {
|
if(item.value === Infinity) {
|
||||||
nstack.push("\\infty")
|
nstack.push("\\infty")
|
||||||
} else if(typeof item.value === 'number' && item.value < 0) {
|
} else if(typeof item.value === "number" && item.value < 0) {
|
||||||
nstack.push(this.par(item.value));
|
nstack.push(this.par(item.value))
|
||||||
} else if(Array.isArray(item.value)) {
|
} else if(Array.isArray(item.value)) {
|
||||||
nstack.push('[' + item.value.map(Modules.ExprParser.Internals.escapeValue).join(', ') + ']');
|
nstack.push("[" + item.value.map(escapeValue).join(", ") + "]")
|
||||||
} else {
|
} else {
|
||||||
nstack.push(Modules.ExprParser.Internals.escapeValue(item.value));
|
nstack.push(escapeValue(item.value))
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
case Modules.ExprParser.Internals.IOP2:
|
case Instruction.IOP2:
|
||||||
n2 = nstack.pop();
|
n2 = nstack.pop()
|
||||||
n1 = nstack.pop();
|
n1 = nstack.pop()
|
||||||
f = item.value;
|
f = item.value
|
||||||
switch(f) {
|
switch(f) {
|
||||||
case '-':
|
case "-":
|
||||||
case '+':
|
case "+":
|
||||||
nstack.push(n1 + f + n2);
|
nstack.push(n1 + f + n2)
|
||||||
break;
|
break
|
||||||
case '||':
|
case "||":
|
||||||
case 'or':
|
case "or":
|
||||||
case '&&':
|
case "&&":
|
||||||
case 'and':
|
case "and":
|
||||||
case '==':
|
case "==":
|
||||||
case '!=':
|
case "!=":
|
||||||
nstack.push(this.par(n1) + f + this.par(n2));
|
nstack.push(this.par(n1) + f + this.par(n2))
|
||||||
break;
|
break
|
||||||
case '*':
|
case "*":
|
||||||
if(n2 == "\\pi" || n2 == "e" || n2 == "x" || n2 == "n")
|
if(n2 === "\\pi" || n2 === "e" || n2 === "x" || n2 === "n")
|
||||||
nstack.push(this.parif(n1,['+','-']) + n2)
|
nstack.push(this.parif(n1, ["+", "-"]) + n2)
|
||||||
else
|
else
|
||||||
nstack.push(this.parif(n1,['+','-']) + " \\times " + this.parif(n2,['+','-']));
|
nstack.push(this.parif(n1, ["+", "-"]) + " \\times " + this.parif(n2, ["+", "-"]))
|
||||||
break;
|
break
|
||||||
case '/':
|
case "/":
|
||||||
nstack.push("\\frac{" + n1 + "}{" + n2 + "}");
|
nstack.push("\\frac{" + n1 + "}{" + n2 + "}")
|
||||||
break;
|
break
|
||||||
case '^':
|
case "^":
|
||||||
nstack.push(this.parif(n1,['+','-','*','/','!']) + "^{" + n2 + "}");
|
nstack.push(this.parif(n1, ["+", "-", "*", "/", "!"]) + "^{" + n2 + "}")
|
||||||
break;
|
break
|
||||||
case '%':
|
case "%":
|
||||||
nstack.push(this.parif(n1,['+','-','*','/','!','^']) + " \\mathrm{mod} " + parif(n2,['+','-','*','/','!','^']));
|
nstack.push(this.parif(n1, ["+", "-", "*", "/", "!", "^"]) + " \\mathrm{mod} " + this.parif(n2, ["+", "-", "*", "/", "!", "^"]))
|
||||||
break;
|
break
|
||||||
case '[':
|
case "[":
|
||||||
nstack.push(n1 + '[' + n2 + ']');
|
nstack.push(n1 + "[" + n2 + "]")
|
||||||
break;
|
break
|
||||||
default:
|
default:
|
||||||
throw new EvalError("Unknown operator " + ope + ".");
|
throw new EvalError("Unknown operator " + item.value + ".")
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
case Modules.ExprParser.Internals.IOP3: // Thirdiary operator
|
case Instruction.IOP3: // Thirdiary operator
|
||||||
n3 = nstack.pop();
|
n3 = nstack.pop()
|
||||||
n2 = nstack.pop();
|
n2 = nstack.pop()
|
||||||
n1 = nstack.pop();
|
n1 = nstack.pop()
|
||||||
f = item.value;
|
f = item.value
|
||||||
if (f === '?') {
|
if(f === "?") {
|
||||||
nstack.push('(' + n1 + ' ? ' + n2 + ' : ' + n3 + ')');
|
nstack.push("(" + n1 + " ? " + n2 + " : " + n3 + ")")
|
||||||
} else {
|
} else {
|
||||||
throw new EvalError('Unknown operator ' + ope + '.');
|
throw new EvalError("Unknown operator " + item.value + ".")
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
case Modules.ExprParser.Internals.IVAR:
|
case Instruction.IVAR:
|
||||||
case Modules.ExprParser.Internals.IVARNAME:
|
case Instruction.IVARNAME:
|
||||||
nstack.push(this.variable(item.value.toString()));
|
nstack.push(this.variable(item.value.toString()))
|
||||||
break;
|
break
|
||||||
case Modules.ExprParser.Internals.IOP1: // Unary operator
|
case Instruction.IOP1: // Unary operator
|
||||||
n1 = nstack.pop();
|
n1 = nstack.pop()
|
||||||
f = item.value;
|
f = item.value
|
||||||
switch(f) {
|
switch(f) {
|
||||||
case '-':
|
case "-":
|
||||||
case '+':
|
case "+":
|
||||||
nstack.push(this.par(f + n1));
|
nstack.push(this.par(f + n1))
|
||||||
break;
|
break
|
||||||
case '!':
|
case "!":
|
||||||
nstack.push(this.parif(n1,['+','-','*','/','^']) + '!');
|
nstack.push(this.parif(n1, ["+", "-", "*", "/", "^"]) + "!")
|
||||||
break;
|
break
|
||||||
default:
|
default:
|
||||||
nstack.push(f + this.parif(n1,['+','-','*','/','^']));
|
nstack.push(f + this.parif(n1, ["+", "-", "*", "/", "^"]))
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
case Modules.ExprParser.Internals.IFUNCALL:
|
case Instruction.IFUNCALL:
|
||||||
argCount = item.value;
|
argCount = item.value
|
||||||
args = [];
|
args = []
|
||||||
while (argCount-- > 0) {
|
while(argCount-- > 0) {
|
||||||
args.unshift(nstack.pop());
|
args.unshift(nstack.pop())
|
||||||
}
|
}
|
||||||
f = nstack.pop();
|
f = nstack.pop()
|
||||||
// Handling various functions
|
// Handling various functions
|
||||||
nstack.push(this.functionToLatex(f, args))
|
nstack.push(this.functionToLatex(f, args))
|
||||||
break;
|
break
|
||||||
case Modules.ExprParser.Internals.IFUNDEF:
|
case Instruction.IMEMBER:
|
||||||
nstack.push(this.par(n1 + '(' + args.join(', ') + ') = ' + n2));
|
n1 = nstack.pop()
|
||||||
break;
|
nstack.push(n1 + "." + item.value)
|
||||||
case Modules.ExprParser.Internals.IMEMBER:
|
break
|
||||||
n1 = nstack.pop();
|
case Instruction.IARRAY:
|
||||||
nstack.push(n1 + '.' + item.value);
|
argCount = item.value
|
||||||
break;
|
args = []
|
||||||
case Modules.ExprParser.Internals.IARRAY:
|
while(argCount-- > 0) {
|
||||||
argCount = item.value;
|
args.unshift(nstack.pop())
|
||||||
args = [];
|
|
||||||
while (argCount-- > 0) {
|
|
||||||
args.unshift(nstack.pop());
|
|
||||||
}
|
}
|
||||||
nstack.push('[' + args.join(', ') + ']');
|
nstack.push("[" + args.join(", ") + "]")
|
||||||
break;
|
break
|
||||||
case Modules.ExprParser.Internals.IEXPR:
|
case Instruction.IEXPR:
|
||||||
nstack.push('(' + this.expression(item.value) + ')');
|
nstack.push("(" + this.expression(item.value) + ")")
|
||||||
break;
|
break
|
||||||
case Modules.ExprParser.Internals.IENDSTATEMENT:
|
case Instruction.IENDSTATEMENT:
|
||||||
break;
|
break
|
||||||
default:
|
default:
|
||||||
throw new EvalError('invalid Expression');
|
throw new EvalError("invalid Expression")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (nstack.length > 1) {
|
if(nstack.length > 1) {
|
||||||
nstack = [ nstack.join(';') ]
|
nstack = [nstack.join(";")]
|
||||||
}
|
}
|
||||||
return String(nstack[0]);
|
return String(nstack[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue