/** * 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 . */ import { Module } from "./common.mjs" import * as Instruction from "../lib/expr-eval/instruction.mjs" import { escapeValue } from "../lib/expr-eval/expression.mjs" import { HelperInterface, LatexInterface } from "./interface.mjs" const unicodechars = [ "α", "β", "γ", "δ", "ε", "ζ", "η", "π", "θ", "κ", "λ", "μ", "ξ", "ρ", "ς", "σ", "τ", "φ", "χ", "ψ", "ω", "Γ", "Δ", "Θ", "Λ", "Ξ", "Π", "Σ", "Φ", "Ψ", "Ω", "ₐ", "ₑ", "ₒ", "ₓ", "ₕ", "ₖ", "ₗ", "ₘ", "ₙ", "ₚ", "ₛ", "ₜ", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹", "⁰", "₁", "₂", "₃", "₄", "₅", "₆", "₇", "₈", "₉", "₀", "pi", "∞"] const equivalchars = [ "\\alpha", "\\beta", "\\gamma", "\\delta", "\\epsilon", "\\zeta", "\\eta", "\\pi", "\\theta", "\\kappa", "\\lambda", "\\mu", "\\xi", "\\rho", "\\sigma", "\\sigma", "\\tau", "\\phi", "\\chi", "\\psi", "\\omega", "\\Gamma", "\\Delta", "\\Theta", "\\Lambda", "\\Xi", "\\Pi", "\\Sigma", "\\Phy", "\\Psi", "\\Omega", "{}_{a}", "{}_{e}", "{}_{o}", "{}_{x}", "{}_{h}", "{}_{k}", "{}_{l}", "{}_{m}", "{}_{n}", "{}_{p}", "{}_{s}", "{}_{t}", "{}^{1}", "{}^{2}", "{}^{3}", "{}^{4}", "{}^{5}", "{}^{6}", "{}^{7}", "{}^{8}", "{}^{9}", "{}^{0}", "{}_{1}", "{}_{2}", "{}_{3}", "{}_{4}", "{}_{5}", "{}_{6}", "{}_{7}", "{}_{8}", "{}_{9}", "{}_{0}", "\\pi", "\\infty"] /** * Class containing the result of a LaTeX render. * * @property {string} source - Exported PNG file * @property {number} width * @property {number} height */ class LatexRenderResult { constructor(source, width, height) { this.source = source this.width = parseFloat(width) this.height = parseFloat(height) } } class LatexAPI extends Module { /** @type {LatexInterface} */ #latex = null constructor() { super("Latex", { latex: LatexInterface, helper: HelperInterface }) /** * true if latex has been enabled by the user, false otherwise. */ this.enabled = false } /** * @param {LatexInterface} latex * @param {HelperInterface} helper */ initialize({ latex, helper }) { super.initialize({ latex, helper }) this.#latex = latex this.enabled = helper.getSettingBool("enable_latex") } /** * Checks if the given markup (with given font size and color) has already been * rendered, and if so, returns its data. Otherwise, returns null. * * @param {string} markup - LaTeX markup to render. * @param {number} fontSize - Font size (in pt) to render. * @param {string} color - Color of the text to render. * @returns {LatexRenderResult|null} */ findPrerendered(markup, fontSize, color) { if(!this.initialized) throw new Error("Attempting findPrerendered before initialize!") const data = this.#latex.findPrerendered(markup, fontSize, color) let ret = null if(data !== "") ret = new LatexRenderResult(...data.split(",")) return ret } /** * Prepares and renders a latex string into a png file asynchronously. * * @param {string} markup - LaTeX markup to render. * @param {number} fontSize - Font size (in pt) to render. * @param {string} color - Color of the text to render. * @returns {Promise} */ async requestAsyncRender(markup, fontSize, color) { if(!this.initialized) throw new Error("Attempting requestAsyncRender before initialize!") let args = this.#latex.render(markup, fontSize, color).split(",") return new LatexRenderResult(...args) } /** * Puts element within parenthesis. * * @param {string|number} elem - element to put within parenthesis. * @returns {string} */ par(elem) { return `(${elem})` } /** * Checks if the element contains at least one of the elements of * the string array contents, but not at the first position of the string, * and returns the parenthesis version if so. * * @param {string|number} elem - element to put within parenthesis. * @param {Array} contents - Array of elements to put within parenthesis. * @returns {string} */ parif(elem, contents) { elem = elem.toString() if(elem[0] !== "(" && elem[elem.length - 1] !== ")" && contents.some(x => elem.indexOf(x) > 0)) return this.par(elem) if(elem[0] === "(" && elem[elem.length - 1] === ")") return elem.removeEnclosure() return elem } /** * Creates a latex expression for a function. * * @param {string} f - Function to convert * @param {(number,string)[]} args - Arguments of the function * @returns {string} */ functionToLatex(f, args) { switch(f) { case "derivative": if(args.length === 3) return `\\frac{d${args[0].removeEnclosure().replaceAll(args[1].removeEnclosure(), "x")}}{dx}` else return `\\frac{d${args[0]}}{dx}(x)` case "integral": if(args.length === 4) return `\\int\\limits_{${args[0]}}^{${args[1]}}${args[2].removeEnclosure()} d${args[3].removeEnclosure()}` else return `\\int\\limits_{${args[0]}}^{${args[1]}}${args[2]}(t) dt` case "sqrt": return `\\sqrt\\left(${args.join(", ")}\\right)` case "abs": return `\\left|${args.join(", ")}\\right|` case "floor": return `\\left\\lfloor${args.join(", ")}\\right\\rfloor` case "ceil": return `\\left\\lceil${args.join(", ")}\\right\\rceil` default: return `\\mathrm{${f}}\\left(${args.join(", ")}\\right)` } } /** * Creates a latex variable from a variable. * * @param {string} vari - variable text to convert * @param {boolean} wrapIn$ - checks whether the escaped chars should be escaped * @returns {string} */ variable(vari, wrapIn$ = false) { if(wrapIn$) for(let i = 0; i < unicodechars.length; i++) { if(vari.includes(unicodechars[i])) vari = vari.replaceAll(unicodechars[i], "$" + equivalchars[i] + "$") } else for(let i = 0; i < unicodechars.length; i++) { if(vari.includes(unicodechars[i])) vari = vari.replaceAll(unicodechars[i], equivalchars[i]) } return vari } /** * Converts expr-eval instructions to a latex string. * * @param {Instruction[]} instructions - expr-eval tokens list * @returns {string} */ expression(instructions) { let nstack = [] let n1, n2, n3 let f, args, argCount for(let item of instructions) { let type = item.type switch(type) { case Instruction.INUMBER: if(item.value === Infinity) { nstack.push("\\infty") } else if(typeof item.value === "number" && item.value < 0) { nstack.push(this.par(item.value)) } else if(Array.isArray(item.value)) { nstack.push("[" + item.value.map(escapeValue).join(", ") + "]") } else { nstack.push(escapeValue(item.value)) } break case Instruction.IOP2: n2 = nstack.pop() n1 = nstack.pop() f = item.value switch(f) { case "-": case "+": nstack.push(n1 + f + n2) break case "||": case "or": case "&&": case "and": case "==": case "!=": nstack.push(this.par(n1) + f + this.par(n2)) break case "*": if(n2 === "\\pi" || n2 === "e" || n2 === "x" || n2 === "n") nstack.push(this.parif(n1, ["+", "-"]) + n2) else nstack.push(this.parif(n1, ["+", "-"]) + " \\times " + this.parif(n2, ["+", "-"])) break case "/": nstack.push("\\frac{" + n1 + "}{" + n2 + "}") break case "^": nstack.push(this.parif(n1, ["+", "-", "*", "/", "!"]) + "^{" + n2 + "}") break case "%": nstack.push(this.parif(n1, ["+", "-", "*", "/", "!", "^"]) + " \\mathrm{mod} " + this.parif(n2, ["+", "-", "*", "/", "!", "^"])) break case "[": nstack.push(n1 + "[" + n2 + "]") break default: throw new EvalError("Unknown operator " + item.value + ".") } break case Instruction.IOP3: // Thirdiary operator n3 = nstack.pop() n2 = nstack.pop() n1 = nstack.pop() f = item.value if(f === "?") { nstack.push("(" + n1 + " ? " + n2 + " : " + n3 + ")") } else { throw new EvalError("Unknown operator " + item.value + ".") } break case Instruction.IVAR: case Instruction.IVARNAME: nstack.push(this.variable(item.value.toString())) break case Instruction.IOP1: // Unary operator n1 = nstack.pop() f = item.value switch(f) { case "-": case "+": nstack.push(this.par(f + n1)) break case "!": nstack.push(this.parif(n1, ["+", "-", "*", "/", "^"]) + "!") break default: nstack.push(f + this.parif(n1, ["+", "-", "*", "/", "^"])) break } break case Instruction.IFUNCALL: argCount = item.value args = [] while(argCount-- > 0) { args.unshift(nstack.pop()) } f = nstack.pop() // Handling various functions nstack.push(this.functionToLatex(f, args)) break case Instruction.IMEMBER: n1 = nstack.pop() nstack.push(n1 + "." + item.value) break case Instruction.IARRAY: argCount = item.value args = [] while(argCount-- > 0) { args.unshift(nstack.pop()) } nstack.push("[" + args.join(", ") + "]") break case Instruction.IEXPR: nstack.push("(" + this.expression(item.value) + ")") break case Instruction.IENDSTATEMENT: break default: throw new EvalError("invalid Expression") } } if(nstack.length > 1) { nstack = [nstack.join(";")] } return String(nstack[0]) } } /** @type {LatexAPI} */ Modules.Latex = Modules.Latex || new LatexAPI() /** @type {LatexAPI} */ export default Modules.Latex