FunctionNode.js (18079B)
1 "use strict"; 2 3 var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); 4 5 Object.defineProperty(exports, "__esModule", { 6 value: true 7 }); 8 exports.createFunctionNode = void 0; 9 10 var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof")); 11 12 var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray")); 13 14 var _is = require("../../utils/is.js"); 15 16 var _string = require("../../utils/string.js"); 17 18 var _object = require("../../utils/object.js"); 19 20 var _customs = require("../../utils/customs.js"); 21 22 var _scope = require("../../utils/scope.js"); 23 24 var _factory = require("../../utils/factory.js"); 25 26 var _latex = require("../../utils/latex.js"); 27 28 var name = 'FunctionNode'; 29 var dependencies = ['math', 'Node', 'SymbolNode']; 30 var createFunctionNode = /* #__PURE__ */(0, _factory.factory)(name, dependencies, function (_ref) { 31 var math = _ref.math, 32 Node = _ref.Node, 33 SymbolNode = _ref.SymbolNode; 34 35 /** 36 * @constructor FunctionNode 37 * @extends {./Node} 38 * invoke a list with arguments on a node 39 * @param {./Node | string} fn Node resolving with a function on which to invoke 40 * the arguments, typically a SymboNode or AccessorNode 41 * @param {./Node[]} args 42 */ 43 function FunctionNode(fn, args) { 44 if (!(this instanceof FunctionNode)) { 45 throw new SyntaxError('Constructor must be called with the new operator'); 46 } 47 48 if (typeof fn === 'string') { 49 fn = new SymbolNode(fn); 50 } // validate input 51 52 53 if (!(0, _is.isNode)(fn)) throw new TypeError('Node expected as parameter "fn"'); 54 55 if (!Array.isArray(args) || !args.every(_is.isNode)) { 56 throw new TypeError('Array containing Nodes expected for parameter "args"'); 57 } 58 59 this.fn = fn; 60 this.args = args || []; // readonly property name 61 62 Object.defineProperty(this, 'name', { 63 get: function () { 64 return this.fn.name || ''; 65 }.bind(this), 66 set: function set() { 67 throw new Error('Cannot assign a new name, name is read-only'); 68 } 69 }); 70 } 71 72 FunctionNode.prototype = new Node(); 73 FunctionNode.prototype.type = 'FunctionNode'; 74 FunctionNode.prototype.isFunctionNode = true; 75 /* format to fixed length */ 76 77 var strin = function strin(entity) { 78 return (0, _string.format)(entity, { 79 truncate: 78 80 }); 81 }; 82 /** 83 * Compile a node into a JavaScript function. 84 * This basically pre-calculates as much as possible and only leaves open 85 * calculations which depend on a dynamic scope with variables. 86 * @param {Object} math Math.js namespace with functions and constants. 87 * @param {Object} argNames An object with argument names as key and `true` 88 * as value. Used in the SymbolNode to optimize 89 * for arguments from user assigned functions 90 * (see FunctionAssignmentNode) or special symbols 91 * like `end` (see IndexNode). 92 * @return {function} Returns a function which can be called like: 93 * evalNode(scope: Object, args: Object, context: *) 94 */ 95 96 97 FunctionNode.prototype._compile = function (math, argNames) { 98 if (!(this instanceof FunctionNode)) { 99 throw new TypeError('No valid FunctionNode'); 100 } // compile arguments 101 102 103 var evalArgs = this.args.map(function (arg) { 104 return arg._compile(math, argNames); 105 }); 106 107 if ((0, _is.isSymbolNode)(this.fn)) { 108 var _name = this.fn.name; 109 110 if (!argNames[_name]) { 111 // we can statically determine whether the function has an rawArgs property 112 var fn = _name in math ? (0, _customs.getSafeProperty)(math, _name) : undefined; 113 var isRaw = typeof fn === 'function' && fn.rawArgs === true; 114 115 var resolveFn = function resolveFn(scope) { 116 var value; 117 118 if (scope.has(_name)) { 119 value = scope.get(_name); 120 } else if (_name in math) { 121 value = (0, _customs.getSafeProperty)(math, _name); 122 } else { 123 return FunctionNode.onUndefinedFunction(_name); 124 } 125 126 if (typeof value === 'function') { 127 return value; 128 } 129 130 throw new TypeError("'".concat(_name, "' is not a function; its value is:\n ").concat(strin(value))); 131 }; 132 133 if (isRaw) { 134 // pass unevaluated parameters (nodes) to the function 135 // "raw" evaluation 136 var rawArgs = this.args; 137 return function evalFunctionNode(scope, args, context) { 138 var fn = resolveFn(scope); 139 return fn(rawArgs, math, (0, _scope.createSubScope)(scope, args), scope); 140 }; 141 } else { 142 // "regular" evaluation 143 switch (evalArgs.length) { 144 case 0: 145 return function evalFunctionNode(scope, args, context) { 146 var fn = resolveFn(scope); 147 return fn(); 148 }; 149 150 case 1: 151 return function evalFunctionNode(scope, args, context) { 152 var fn = resolveFn(scope); 153 var evalArg0 = evalArgs[0]; 154 return fn(evalArg0(scope, args, context)); 155 }; 156 157 case 2: 158 return function evalFunctionNode(scope, args, context) { 159 var fn = resolveFn(scope); 160 var evalArg0 = evalArgs[0]; 161 var evalArg1 = evalArgs[1]; 162 return fn(evalArg0(scope, args, context), evalArg1(scope, args, context)); 163 }; 164 165 default: 166 return function evalFunctionNode(scope, args, context) { 167 var fn = resolveFn(scope); 168 var values = evalArgs.map(function (evalArg) { 169 return evalArg(scope, args, context); 170 }); 171 return fn.apply(void 0, (0, _toConsumableArray2.default)(values)); 172 }; 173 } 174 } 175 } else { 176 // the function symbol is an argName 177 var _rawArgs = this.args; 178 return function evalFunctionNode(scope, args, context) { 179 var fn = args[_name]; 180 181 if (typeof fn !== 'function') { 182 throw new TypeError("Argument '".concat(_name, "' was not a function; received: ").concat(strin(fn))); 183 } 184 185 if (fn.rawArgs) { 186 return fn(_rawArgs, math, (0, _scope.createSubScope)(scope, args), scope); // "raw" evaluation 187 } else { 188 var values = evalArgs.map(function (evalArg) { 189 return evalArg(scope, args, context); 190 }); 191 return fn.apply(fn, values); 192 } 193 }; 194 } 195 } else if ((0, _is.isAccessorNode)(this.fn) && (0, _is.isIndexNode)(this.fn.index) && this.fn.index.isObjectProperty()) { 196 // execute the function with the right context: the object of the AccessorNode 197 var evalObject = this.fn.object._compile(math, argNames); 198 199 var prop = this.fn.index.getObjectProperty(); 200 var _rawArgs2 = this.args; 201 return function evalFunctionNode(scope, args, context) { 202 var object = evalObject(scope, args, context); 203 (0, _customs.validateSafeMethod)(object, prop); 204 var isRaw = object[prop] && object[prop].rawArgs; 205 206 if (isRaw) { 207 return object[prop](_rawArgs2, math, (0, _scope.createSubScope)(scope, args), scope); // "raw" evaluation 208 } else { 209 // "regular" evaluation 210 var values = evalArgs.map(function (evalArg) { 211 return evalArg(scope, args, context); 212 }); 213 return object[prop].apply(object, values); 214 } 215 }; 216 } else { 217 // node.fn.isAccessorNode && !node.fn.index.isObjectProperty() 218 // we have to dynamically determine whether the function has a rawArgs property 219 var fnExpr = this.fn.toString(); 220 221 var evalFn = this.fn._compile(math, argNames); 222 223 var _rawArgs3 = this.args; 224 return function evalFunctionNode(scope, args, context) { 225 var fn = evalFn(scope, args, context); 226 227 if (typeof fn !== 'function') { 228 throw new TypeError("Expression '".concat(fnExpr, "' did not evaluate to a function; value is:") + "\n ".concat(strin(fn))); 229 } 230 231 if (fn.rawArgs) { 232 return fn(_rawArgs3, math, (0, _scope.createSubScope)(scope, args), scope); // "raw" evaluation 233 } else { 234 // "regular" evaluation 235 var values = evalArgs.map(function (evalArg) { 236 return evalArg(scope, args, context); 237 }); 238 return fn.apply(fn, values); 239 } 240 }; 241 } 242 }; 243 /** 244 * Execute a callback for each of the child nodes of this node 245 * @param {function(child: Node, path: string, parent: Node)} callback 246 */ 247 248 249 FunctionNode.prototype.forEach = function (callback) { 250 callback(this.fn, 'fn', this); 251 252 for (var i = 0; i < this.args.length; i++) { 253 callback(this.args[i], 'args[' + i + ']', this); 254 } 255 }; 256 /** 257 * Create a new FunctionNode having it's childs be the results of calling 258 * the provided callback function for each of the childs of the original node. 259 * @param {function(child: Node, path: string, parent: Node): Node} callback 260 * @returns {FunctionNode} Returns a transformed copy of the node 261 */ 262 263 264 FunctionNode.prototype.map = function (callback) { 265 var fn = this._ifNode(callback(this.fn, 'fn', this)); 266 267 var args = []; 268 269 for (var i = 0; i < this.args.length; i++) { 270 args[i] = this._ifNode(callback(this.args[i], 'args[' + i + ']', this)); 271 } 272 273 return new FunctionNode(fn, args); 274 }; 275 /** 276 * Create a clone of this node, a shallow copy 277 * @return {FunctionNode} 278 */ 279 280 281 FunctionNode.prototype.clone = function () { 282 return new FunctionNode(this.fn, this.args.slice(0)); 283 }; 284 /** 285 * Throws an error 'Undefined function {name}' 286 * @param {string} name 287 */ 288 289 290 FunctionNode.onUndefinedFunction = function (name) { 291 throw new Error('Undefined function ' + name); 292 }; // backup Node's toString function 293 // @private 294 295 296 var nodeToString = FunctionNode.prototype.toString; 297 /** 298 * Get string representation. (wrapper function) 299 * This overrides parts of Node's toString function. 300 * If callback is an object containing callbacks, it 301 * calls the correct callback for the current node, 302 * otherwise it falls back to calling Node's toString 303 * function. 304 * 305 * @param {Object} options 306 * @return {string} str 307 * @override 308 */ 309 310 FunctionNode.prototype.toString = function (options) { 311 var customString; 312 var name = this.fn.toString(options); 313 314 if (options && (0, _typeof2.default)(options.handler) === 'object' && (0, _object.hasOwnProperty)(options.handler, name)) { 315 // callback is a map of callback functions 316 customString = options.handler[name](this, options); 317 } 318 319 if (typeof customString !== 'undefined') { 320 return customString; 321 } // fall back to Node's toString 322 323 324 return nodeToString.call(this, options); 325 }; 326 /** 327 * Get string representation 328 * @param {Object} options 329 * @return {string} str 330 */ 331 332 333 FunctionNode.prototype._toString = function (options) { 334 var args = this.args.map(function (arg) { 335 return arg.toString(options); 336 }); 337 var fn = (0, _is.isFunctionAssignmentNode)(this.fn) ? '(' + this.fn.toString(options) + ')' : this.fn.toString(options); // format the arguments like "add(2, 4.2)" 338 339 return fn + '(' + args.join(', ') + ')'; 340 }; 341 /** 342 * Get a JSON representation of the node 343 * @returns {Object} 344 */ 345 346 347 FunctionNode.prototype.toJSON = function () { 348 return { 349 mathjs: 'FunctionNode', 350 fn: this.fn, 351 args: this.args 352 }; 353 }; 354 /** 355 * Instantiate an AssignmentNode from its JSON representation 356 * @param {Object} json An object structured like 357 * `{"mathjs": "FunctionNode", fn: ..., args: ...}`, 358 * where mathjs is optional 359 * @returns {FunctionNode} 360 */ 361 362 363 FunctionNode.fromJSON = function (json) { 364 return new FunctionNode(json.fn, json.args); 365 }; 366 /** 367 * Get HTML representation 368 * @param {Object} options 369 * @return {string} str 370 */ 371 372 373 FunctionNode.prototype.toHTML = function (options) { 374 var args = this.args.map(function (arg) { 375 return arg.toHTML(options); 376 }); // format the arguments like "add(2, 4.2)" 377 378 return '<span class="math-function">' + (0, _string.escape)(this.fn) + '</span><span class="math-paranthesis math-round-parenthesis">(</span>' + args.join('<span class="math-separator">,</span>') + '<span class="math-paranthesis math-round-parenthesis">)</span>'; 379 }; 380 /* 381 * Expand a LaTeX template 382 * 383 * @param {string} template 384 * @param {Node} node 385 * @param {Object} options 386 * @private 387 **/ 388 389 390 function expandTemplate(template, node, options) { 391 var latex = ''; // Match everything of the form ${identifier} or ${identifier[2]} or $$ 392 // while submatching identifier and 2 (in the second case) 393 394 var regex = /\$(?:\{([a-z_][a-z_0-9]*)(?:\[([0-9]+)\])?\}|\$)/gi; 395 var inputPos = 0; // position in the input string 396 397 var match; 398 399 while ((match = regex.exec(template)) !== null) { 400 // go through all matches 401 // add everything in front of the match to the LaTeX string 402 latex += template.substring(inputPos, match.index); 403 inputPos = match.index; 404 405 if (match[0] === '$$') { 406 // escaped dollar sign 407 latex += '$'; 408 inputPos++; 409 } else { 410 // template parameter 411 inputPos += match[0].length; 412 var property = node[match[1]]; 413 414 if (!property) { 415 throw new ReferenceError('Template: Property ' + match[1] + ' does not exist.'); 416 } 417 418 if (match[2] === undefined) { 419 // no square brackets 420 switch ((0, _typeof2.default)(property)) { 421 case 'string': 422 latex += property; 423 break; 424 425 case 'object': 426 if ((0, _is.isNode)(property)) { 427 latex += property.toTex(options); 428 } else if (Array.isArray(property)) { 429 // make array of Nodes into comma separated list 430 latex += property.map(function (arg, index) { 431 if ((0, _is.isNode)(arg)) { 432 return arg.toTex(options); 433 } 434 435 throw new TypeError('Template: ' + match[1] + '[' + index + '] is not a Node.'); 436 }).join(','); 437 } else { 438 throw new TypeError('Template: ' + match[1] + ' has to be a Node, String or array of Nodes'); 439 } 440 441 break; 442 443 default: 444 throw new TypeError('Template: ' + match[1] + ' has to be a Node, String or array of Nodes'); 445 } 446 } else { 447 // with square brackets 448 if ((0, _is.isNode)(property[match[2]] && property[match[2]])) { 449 latex += property[match[2]].toTex(options); 450 } else { 451 throw new TypeError('Template: ' + match[1] + '[' + match[2] + '] is not a Node.'); 452 } 453 } 454 } 455 } 456 457 latex += template.slice(inputPos); // append rest of the template 458 459 return latex; 460 } // backup Node's toTex function 461 // @private 462 463 464 var nodeToTex = FunctionNode.prototype.toTex; 465 /** 466 * Get LaTeX representation. (wrapper function) 467 * This overrides parts of Node's toTex function. 468 * If callback is an object containing callbacks, it 469 * calls the correct callback for the current node, 470 * otherwise it falls back to calling Node's toTex 471 * function. 472 * 473 * @param {Object} options 474 * @return {string} 475 */ 476 477 FunctionNode.prototype.toTex = function (options) { 478 var customTex; 479 480 if (options && (0, _typeof2.default)(options.handler) === 'object' && (0, _object.hasOwnProperty)(options.handler, this.name)) { 481 // callback is a map of callback functions 482 customTex = options.handler[this.name](this, options); 483 } 484 485 if (typeof customTex !== 'undefined') { 486 return customTex; 487 } // fall back to Node's toTex 488 489 490 return nodeToTex.call(this, options); 491 }; 492 /** 493 * Get LaTeX representation 494 * @param {Object} options 495 * @return {string} str 496 */ 497 498 499 FunctionNode.prototype._toTex = function (options) { 500 var args = this.args.map(function (arg) { 501 // get LaTeX of the arguments 502 return arg.toTex(options); 503 }); 504 var latexConverter; 505 506 if (_latex.latexFunctions[this.name]) { 507 latexConverter = _latex.latexFunctions[this.name]; 508 } // toTex property on the function itself 509 510 511 if (math[this.name] && (typeof math[this.name].toTex === 'function' || (0, _typeof2.default)(math[this.name].toTex) === 'object' || typeof math[this.name].toTex === 'string')) { 512 // .toTex is a callback function 513 latexConverter = math[this.name].toTex; 514 } 515 516 var customToTex; 517 518 switch ((0, _typeof2.default)(latexConverter)) { 519 case 'function': 520 // a callback function 521 customToTex = latexConverter(this, options); 522 break; 523 524 case 'string': 525 // a template string 526 customToTex = expandTemplate(latexConverter, this, options); 527 break; 528 529 case 'object': 530 // an object with different "converters" for different numbers of arguments 531 switch ((0, _typeof2.default)(latexConverter[args.length])) { 532 case 'function': 533 customToTex = latexConverter[args.length](this, options); 534 break; 535 536 case 'string': 537 customToTex = expandTemplate(latexConverter[args.length], this, options); 538 break; 539 } 540 541 } 542 543 if (typeof customToTex !== 'undefined') { 544 return customToTex; 545 } 546 547 return expandTemplate(_latex.defaultTemplate, this, options); 548 }; 549 /** 550 * Get identifier. 551 * @return {string} 552 */ 553 554 555 FunctionNode.prototype.getIdentifier = function () { 556 return this.type + ':' + this.name; 557 }; 558 559 return FunctionNode; 560 }, { 561 isClass: true, 562 isNode: true 563 }); 564 exports.createFunctionNode = createFunctionNode;