import { type NodeInput, type NodeExpr, type NodeIdentifier, type BuiltInFunctionTable, CharString, ComplexDecimal, AST, Scope, CallFrame } from 'mathjslab'; import { insertOutput } from './outputFunction'; import { PlotEngine } from './PlotEngine'; import { openFileDialog } from './openFileDialog'; import { Markdown } from './Markdown'; import { appEngine } from './appEngine'; const openFileOptionMathJSLab: OpenFilePickerOptions & { multiple?: false | undefined } = { multiple: false, types: [ { description: 'MathJSLab files', accept: { 'text/plain': ['.txt', '.m'] }, }, ], excludeAcceptAllOption: true, }; const openFileOptionMarkdown: OpenFilePickerOptions & { multiple?: false | undefined } = { multiple: false, types: [ { description: 'Markdown files', accept: { 'text/plain': ['.txt', '.md'] }, }, ], excludeAcceptAllOption: true, }; /** * Browser-facing built-ins registered in addition to the core MathJSLab * function table. */ const externalFunctionTable: BuiltInFunctionTable = { ...PlotEngine.externalFunctionTable, summation: { type: 'BUILTIN', id: 'summation', mapper: false, ev: [false, true, true, false], func: (variable: NodeIdentifier, start: ComplexDecimal, end: ComplexDecimal, expr: NodeExpr): ComplexDecimal => { if (!start.im.eq(0)) throw new Error('complex number sum index'); if (!end.im.eq(0)) throw new Error('complex number sum index'); let result: ComplexDecimal = ComplexDecimal.zero(); /* Create a local scope for the summation variable. */ const sumScope = Scope.create(appEngine.interpreter.context.currentScope); /* Push the summation scope while evaluating the expression. */ appEngine.interpreter.context.callStack!.push(new CallFrame(sumScope)); for (let i = start.re.toNumber(); i <= end.re.toNumber(); i++) { const value = ComplexDecimal.create(i, 0); sumScope.defineName(variable.id, value); const evalResult = appEngine.interpreter.Evaluator(expr, sumScope) as ComplexDecimal; result = ComplexDecimal.add(result, evalResult); } /* Restore the call stack after evaluating the summation. */ appEngine.interpreter.context.callStack!.pop(); return result; }, UnparserMathML: (tree: NodeInput): string => { return ( '' + appEngine.interpreter.UnparserMathML(tree.args[0]) + '=' + appEngine.interpreter.UnparserMathML(tree.args[1]) + '' + appEngine.interpreter.UnparserMathML(tree.args[2]) + '' + '' + appEngine.interpreter.UnparserMathML(tree.args[3]) + '' ); }, }, productory: { type: 'BUILTIN', id: 'productory', mapper: false, ev: [false, true, true, false], func: (variable: NodeIdentifier, start: ComplexDecimal, end: ComplexDecimal, expr: NodeExpr): ComplexDecimal => { if (!start.im.eq(0)) throw new Error('complex number prod index'); if (!end.im.eq(0)) throw new Error('complex number prod index'); let result: ComplexDecimal = ComplexDecimal.one(); const context = appEngine.interpreter.context; /* Create a local scope for the product variable. */ const localScope = Scope.create(context.currentScope); /* Push the product scope while evaluating the expression. */ context.callStack!.push(new CallFrame(localScope)); try { for (let i = start.re.toNumber(); i <= end.re.toNumber(); i++) { /* Assign the iteration value inside the local scope. */ localScope.defineName(variable.id, ComplexDecimal.create(i, 0)); /* Evaluate the expression with the product scope active. */ const value = appEngine.interpreter.Evaluator(expr) as ComplexDecimal; result = ComplexDecimal.mul(result, value); } } finally { /* Always restore the call stack, including error paths. */ context.callStack!.pop(); } return result; }, UnparserMathML: (tree: NodeInput): string => { return ( '' + appEngine.interpreter.UnparserMathML(tree.args[0]) + '=' + appEngine.interpreter.UnparserMathML(tree.args[1]) + '' + appEngine.interpreter.UnparserMathML(tree.args[2]) + '' + '' + appEngine.interpreter.UnparserMathML(tree.args[3]) + '' ); }, }, open: { type: 'BUILTIN', id: 'open', mapper: false, ev: [true], func: (url?: CharString): NodeExpr => { const promptEntry = appEngine.shell.commandShell.element.promptSet.currentPrompt; if (url) { if (appEngine.shell.isFileProtocol) { promptEntry.element.frameBox.className = 'bad'; promptEntry.element.output.innerHTML = 'open function unavailable offline.'; } else { globalThis .fetch(url.str) .then((response) => { if (response.ok) { return response.text(); } else { throw new URIError('Load error.'); } }) .then((responseFile: string) => { appEngine.shell.commandShell.load(responseFile); }) /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ .catch((error) => { promptEntry.element.frameBox.className = 'bad'; promptEntry.element.output.innerHTML = `open: error loading ${url.str}`; }); } return AST.nodeIndexExpr(AST.nodeIdentifier('open'), AST.nodeList([url.str])); } else { openFileDialog((content: string) => { appEngine.shell.commandShell.load(content); }, openFileOptionMathJSLab); return AST.nodeIndexExpr(AST.nodeIdentifier('open'), AST.nodeListFirst()); } }, }, markdown: { type: 'BUILTIN', id: 'markdown', mapper: false, ev: [true], func: (url?: CharString): NodeExpr => { const promptEntry = appEngine.shell.commandShell.element.promptSet.currentPrompt; if (url) { if (appEngine.shell.isFileProtocol) { promptEntry.element.frameBox.className = 'bad'; promptEntry.element.output.innerHTML = 'markdown function unavailable offline.'; } else { globalThis .fetch(url.str) .then((response) => { if (response.ok) { return response.text(); } else { throw new URIError('Load error.'); } }) .then((responseFile: string) => { promptEntry.element.frameBox.className = 'doc'; promptEntry.element.output.innerHTML = Markdown.parse(responseFile); Markdown.typeset(promptEntry.element.output); }) /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ .catch((error) => { promptEntry.element.frameBox.className = 'bad'; promptEntry.element.output.innerHTML = `markdown: error loading ${url.str}`; }); } return AST.nodeIndexExpr(AST.nodeIdentifier('markdown'), AST.nodeList([url.str])); } else { openFileDialog((content: string) => { promptEntry.element.frameBox.className = 'doc'; promptEntry.element.output.innerHTML = Markdown.parse(content); Markdown.typeset(promptEntry.element.output); }, openFileOptionMarkdown); return AST.nodeIndexExpr(AST.nodeIdentifier('markdown'), AST.nodeListFirst()); } }, }, load: { type: 'BUILTIN', id: 'load', mapper: false, ev: [true], func: (...url: CharString[]): NodeExpr => { const promptEntry = appEngine.shell.commandShell.element.promptSet.currentPrompt; const loadContent = (content: string, name: string) => { let error: boolean = false; let errorMessage: string = ''; insertOutput.type = ''; promptEntry.element.output.innerHTML = ''; try { const tree = appEngine.interpreter.Parse(content); if (tree) { appEngine.interpreter.Evaluate(tree); } } catch (e) { error = true; errorMessage = `load: error loading ${name}: ${e}`; } if (error) { promptEntry.element.frameBox.className = 'bad'; promptEntry.element.output.innerHTML = errorMessage; } else { promptEntry.element.frameBox.className = 'good'; promptEntry.element.output.innerHTML = `Loaded script from ${name}`; } appEngine.shell.commandShell.refreshNameList(); }; if (url.length > 0) { if (appEngine.shell.isFileProtocol) { promptEntry.element.frameBox.className = 'bad'; promptEntry.element.output.innerHTML = 'load function unavailable offline.'; } else { url.forEach((file: CharString) => { globalThis .fetch(file.str) .then((response) => { if (response.ok) { return response.text(); } else { throw new URIError('Load error.'); } }) .then((responseFile: string) => { loadContent(responseFile, file.str); }) /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ .catch((error) => { promptEntry.element.frameBox.className = 'bad'; promptEntry.element.output.innerHTML = `load: error loading ${file.str}`; }); }); } return AST.nodeIndexExpr(AST.nodeIdentifier('load'), AST.nodeList([...url.map((url) => AST.nodeString(url.str))])); } else { openFileDialog((content: string) => { loadContent(content, 'file'); }, openFileOptionMathJSLab); return AST.nodeIndexExpr(AST.nodeIdentifier('load'), AST.nodeListFirst()); } }, }, }; export { externalFunctionTable }; export default { externalFunctionTable };