OperatorNode.js (24177B)
1 "use strict"; 2 3 Object.defineProperty(exports, "__esModule", { 4 value: true 5 }); 6 exports.createOperatorNode = void 0; 7 8 var _is = require("../../utils/is.js"); 9 10 var _array = require("../../utils/array.js"); 11 12 var _string = require("../../utils/string.js"); 13 14 var _customs = require("../../utils/customs.js"); 15 16 var _operators = require("../operators.js"); 17 18 var _latex = require("../../utils/latex.js"); 19 20 var _factory = require("../../utils/factory.js"); 21 22 var name = 'OperatorNode'; 23 var dependencies = ['Node']; 24 var createOperatorNode = /* #__PURE__ */(0, _factory.factory)(name, dependencies, function (_ref) { 25 var Node = _ref.Node; 26 27 /** 28 * @constructor OperatorNode 29 * @extends {Node} 30 * An operator with two arguments, like 2+3 31 * 32 * @param {string} op Operator name, for example '+' 33 * @param {string} fn Function name, for example 'add' 34 * @param {Node[]} args Operator arguments 35 * @param {boolean} [implicit] Is this an implicit multiplication? 36 * @param {boolean} [isPercentage] Is this an percentage Operation? 37 */ 38 function OperatorNode(op, fn, args, implicit, isPercentage) { 39 if (!(this instanceof OperatorNode)) { 40 throw new SyntaxError('Constructor must be called with the new operator'); 41 } // validate input 42 43 44 if (typeof op !== 'string') { 45 throw new TypeError('string expected for parameter "op"'); 46 } 47 48 if (typeof fn !== 'string') { 49 throw new TypeError('string expected for parameter "fn"'); 50 } 51 52 if (!Array.isArray(args) || !args.every(_is.isNode)) { 53 throw new TypeError('Array containing Nodes expected for parameter "args"'); 54 } 55 56 this.implicit = implicit === true; 57 this.isPercentage = isPercentage === true; 58 this.op = op; 59 this.fn = fn; 60 this.args = args || []; 61 } 62 63 OperatorNode.prototype = new Node(); 64 OperatorNode.prototype.type = 'OperatorNode'; 65 OperatorNode.prototype.isOperatorNode = true; 66 /** 67 * Compile a node into a JavaScript function. 68 * This basically pre-calculates as much as possible and only leaves open 69 * calculations which depend on a dynamic scope with variables. 70 * @param {Object} math Math.js namespace with functions and constants. 71 * @param {Object} argNames An object with argument names as key and `true` 72 * as value. Used in the SymbolNode to optimize 73 * for arguments from user assigned functions 74 * (see FunctionAssignmentNode) or special symbols 75 * like `end` (see IndexNode). 76 * @return {function} Returns a function which can be called like: 77 * evalNode(scope: Object, args: Object, context: *) 78 */ 79 80 OperatorNode.prototype._compile = function (math, argNames) { 81 // validate fn 82 if (typeof this.fn !== 'string' || !(0, _customs.isSafeMethod)(math, this.fn)) { 83 if (!math[this.fn]) { 84 throw new Error('Function ' + this.fn + ' missing in provided namespace "math"'); 85 } else { 86 throw new Error('No access to function "' + this.fn + '"'); 87 } 88 } 89 90 var fn = (0, _customs.getSafeProperty)(math, this.fn); 91 var evalArgs = (0, _array.map)(this.args, function (arg) { 92 return arg._compile(math, argNames); 93 }); 94 95 if (evalArgs.length === 1) { 96 var evalArg0 = evalArgs[0]; 97 return function evalOperatorNode(scope, args, context) { 98 return fn(evalArg0(scope, args, context)); 99 }; 100 } else if (evalArgs.length === 2) { 101 var _evalArg = evalArgs[0]; 102 var evalArg1 = evalArgs[1]; 103 return function evalOperatorNode(scope, args, context) { 104 return fn(_evalArg(scope, args, context), evalArg1(scope, args, context)); 105 }; 106 } else { 107 return function evalOperatorNode(scope, args, context) { 108 return fn.apply(null, (0, _array.map)(evalArgs, function (evalArg) { 109 return evalArg(scope, args, context); 110 })); 111 }; 112 } 113 }; 114 /** 115 * Execute a callback for each of the child nodes of this node 116 * @param {function(child: Node, path: string, parent: Node)} callback 117 */ 118 119 120 OperatorNode.prototype.forEach = function (callback) { 121 for (var i = 0; i < this.args.length; i++) { 122 callback(this.args[i], 'args[' + i + ']', this); 123 } 124 }; 125 /** 126 * Create a new OperatorNode having it's childs be the results of calling 127 * the provided callback function for each of the childs of the original node. 128 * @param {function(child: Node, path: string, parent: Node): Node} callback 129 * @returns {OperatorNode} Returns a transformed copy of the node 130 */ 131 132 133 OperatorNode.prototype.map = function (callback) { 134 var args = []; 135 136 for (var i = 0; i < this.args.length; i++) { 137 args[i] = this._ifNode(callback(this.args[i], 'args[' + i + ']', this)); 138 } 139 140 return new OperatorNode(this.op, this.fn, args, this.implicit, this.isPercentage); 141 }; 142 /** 143 * Create a clone of this node, a shallow copy 144 * @return {OperatorNode} 145 */ 146 147 148 OperatorNode.prototype.clone = function () { 149 return new OperatorNode(this.op, this.fn, this.args.slice(0), this.implicit, this.isPercentage); 150 }; 151 /** 152 * Check whether this is an unary OperatorNode: 153 * has exactly one argument, like `-a`. 154 * @return {boolean} Returns true when an unary operator node, false otherwise. 155 */ 156 157 158 OperatorNode.prototype.isUnary = function () { 159 return this.args.length === 1; 160 }; 161 /** 162 * Check whether this is a binary OperatorNode: 163 * has exactly two arguments, like `a + b`. 164 * @return {boolean} Returns true when a binary operator node, false otherwise. 165 */ 166 167 168 OperatorNode.prototype.isBinary = function () { 169 return this.args.length === 2; 170 }; 171 /** 172 * Calculate which parentheses are necessary. Gets an OperatorNode 173 * (which is the root of the tree) and an Array of Nodes 174 * (this.args) and returns an array where 'true' means that an argument 175 * has to be enclosed in parentheses whereas 'false' means the opposite. 176 * 177 * @param {OperatorNode} root 178 * @param {string} parenthesis 179 * @param {Node[]} args 180 * @param {boolean} latex 181 * @return {boolean[]} 182 * @private 183 */ 184 185 186 function calculateNecessaryParentheses(root, parenthesis, implicit, args, latex) { 187 // precedence of the root OperatorNode 188 var precedence = (0, _operators.getPrecedence)(root, parenthesis); 189 var associativity = (0, _operators.getAssociativity)(root, parenthesis); 190 191 if (parenthesis === 'all' || args.length > 2 && root.getIdentifier() !== 'OperatorNode:add' && root.getIdentifier() !== 'OperatorNode:multiply') { 192 return args.map(function (arg) { 193 switch (arg.getContent().type) { 194 // Nodes that don't need extra parentheses 195 case 'ArrayNode': 196 case 'ConstantNode': 197 case 'SymbolNode': 198 case 'ParenthesisNode': 199 return false; 200 201 default: 202 return true; 203 } 204 }); 205 } 206 207 var result; 208 209 switch (args.length) { 210 case 0: 211 result = []; 212 break; 213 214 case 1: 215 // unary operators 216 { 217 // precedence of the operand 218 var operandPrecedence = (0, _operators.getPrecedence)(args[0], parenthesis); // handle special cases for LaTeX, where some of the parentheses aren't needed 219 220 if (latex && operandPrecedence !== null) { 221 var operandIdentifier; 222 var rootIdentifier; 223 224 if (parenthesis === 'keep') { 225 operandIdentifier = args[0].getIdentifier(); 226 rootIdentifier = root.getIdentifier(); 227 } else { 228 // Ignore Parenthesis Nodes when not in 'keep' mode 229 operandIdentifier = args[0].getContent().getIdentifier(); 230 rootIdentifier = root.getContent().getIdentifier(); 231 } 232 233 if (_operators.properties[precedence][rootIdentifier].latexLeftParens === false) { 234 result = [false]; 235 break; 236 } 237 238 if (_operators.properties[operandPrecedence][operandIdentifier].latexParens === false) { 239 result = [false]; 240 break; 241 } 242 } 243 244 if (operandPrecedence === null) { 245 // if the operand has no defined precedence, no parens are needed 246 result = [false]; 247 break; 248 } 249 250 if (operandPrecedence <= precedence) { 251 // if the operands precedence is lower, parens are needed 252 result = [true]; 253 break; 254 } // otherwise, no parens needed 255 256 257 result = [false]; 258 } 259 break; 260 261 case 2: 262 // binary operators 263 { 264 var lhsParens; // left hand side needs parenthesis? 265 // precedence of the left hand side 266 267 var lhsPrecedence = (0, _operators.getPrecedence)(args[0], parenthesis); // is the root node associative with the left hand side 268 269 var assocWithLhs = (0, _operators.isAssociativeWith)(root, args[0], parenthesis); 270 271 if (lhsPrecedence === null) { 272 // if the left hand side has no defined precedence, no parens are needed 273 // FunctionNode for example 274 lhsParens = false; 275 } else if (lhsPrecedence === precedence && associativity === 'right' && !assocWithLhs) { 276 // In case of equal precedence, if the root node is left associative 277 // parens are **never** necessary for the left hand side. 278 // If it is right associative however, parens are necessary 279 // if the root node isn't associative with the left hand side 280 lhsParens = true; 281 } else if (lhsPrecedence < precedence) { 282 lhsParens = true; 283 } else { 284 lhsParens = false; 285 } 286 287 var rhsParens; // right hand side needs parenthesis? 288 // precedence of the right hand side 289 290 var rhsPrecedence = (0, _operators.getPrecedence)(args[1], parenthesis); // is the root node associative with the right hand side? 291 292 var assocWithRhs = (0, _operators.isAssociativeWith)(root, args[1], parenthesis); 293 294 if (rhsPrecedence === null) { 295 // if the right hand side has no defined precedence, no parens are needed 296 // FunctionNode for example 297 rhsParens = false; 298 } else if (rhsPrecedence === precedence && associativity === 'left' && !assocWithRhs) { 299 // In case of equal precedence, if the root node is right associative 300 // parens are **never** necessary for the right hand side. 301 // If it is left associative however, parens are necessary 302 // if the root node isn't associative with the right hand side 303 rhsParens = true; 304 } else if (rhsPrecedence < precedence) { 305 rhsParens = true; 306 } else { 307 rhsParens = false; 308 } // handle special cases for LaTeX, where some of the parentheses aren't needed 309 310 311 if (latex) { 312 var _rootIdentifier; 313 314 var lhsIdentifier; 315 var rhsIdentifier; 316 317 if (parenthesis === 'keep') { 318 _rootIdentifier = root.getIdentifier(); 319 lhsIdentifier = root.args[0].getIdentifier(); 320 rhsIdentifier = root.args[1].getIdentifier(); 321 } else { 322 // Ignore ParenthesisNodes when not in 'keep' mode 323 _rootIdentifier = root.getContent().getIdentifier(); 324 lhsIdentifier = root.args[0].getContent().getIdentifier(); 325 rhsIdentifier = root.args[1].getContent().getIdentifier(); 326 } 327 328 if (lhsPrecedence !== null) { 329 if (_operators.properties[precedence][_rootIdentifier].latexLeftParens === false) { 330 lhsParens = false; 331 } 332 333 if (_operators.properties[lhsPrecedence][lhsIdentifier].latexParens === false) { 334 lhsParens = false; 335 } 336 } 337 338 if (rhsPrecedence !== null) { 339 if (_operators.properties[precedence][_rootIdentifier].latexRightParens === false) { 340 rhsParens = false; 341 } 342 343 if (_operators.properties[rhsPrecedence][rhsIdentifier].latexParens === false) { 344 rhsParens = false; 345 } 346 } 347 } 348 349 result = [lhsParens, rhsParens]; 350 } 351 break; 352 353 default: 354 if (root.getIdentifier() === 'OperatorNode:add' || root.getIdentifier() === 'OperatorNode:multiply') { 355 result = args.map(function (arg) { 356 var argPrecedence = (0, _operators.getPrecedence)(arg, parenthesis); 357 var assocWithArg = (0, _operators.isAssociativeWith)(root, arg, parenthesis); 358 var argAssociativity = (0, _operators.getAssociativity)(arg, parenthesis); 359 360 if (argPrecedence === null) { 361 // if the argument has no defined precedence, no parens are needed 362 return false; 363 } else if (precedence === argPrecedence && associativity === argAssociativity && !assocWithArg) { 364 return true; 365 } else if (argPrecedence < precedence) { 366 return true; 367 } 368 369 return false; 370 }); 371 } 372 373 break; 374 } // handles an edge case of 'auto' parentheses with implicit multiplication of ConstantNode 375 // In that case print parentheses for ParenthesisNodes even though they normally wouldn't be 376 // printed. 377 378 379 if (args.length >= 2 && root.getIdentifier() === 'OperatorNode:multiply' && root.implicit && parenthesis === 'auto' && implicit === 'hide') { 380 result = args.map(function (arg, index) { 381 var isParenthesisNode = arg.getIdentifier() === 'ParenthesisNode'; 382 383 if (result[index] || isParenthesisNode) { 384 // put in parenthesis? 385 return true; 386 } 387 388 return false; 389 }); 390 } 391 392 return result; 393 } 394 /** 395 * Get string representation. 396 * @param {Object} options 397 * @return {string} str 398 */ 399 400 401 OperatorNode.prototype._toString = function (options) { 402 var parenthesis = options && options.parenthesis ? options.parenthesis : 'keep'; 403 var implicit = options && options.implicit ? options.implicit : 'hide'; 404 var args = this.args; 405 var parens = calculateNecessaryParentheses(this, parenthesis, implicit, args, false); 406 407 if (args.length === 1) { 408 // unary operators 409 var assoc = (0, _operators.getAssociativity)(this, parenthesis); 410 var operand = args[0].toString(options); 411 412 if (parens[0]) { 413 operand = '(' + operand + ')'; 414 } // for example for "not", we want a space between operand and argument 415 416 417 var opIsNamed = /[a-zA-Z]+/.test(this.op); 418 419 if (assoc === 'right') { 420 // prefix operator 421 return this.op + (opIsNamed ? ' ' : '') + operand; 422 } else if (assoc === 'left') { 423 // postfix 424 return operand + (opIsNamed ? ' ' : '') + this.op; 425 } // fall back to postfix 426 427 428 return operand + this.op; 429 } else if (args.length === 2) { 430 var lhs = args[0].toString(options); // left hand side 431 432 var rhs = args[1].toString(options); // right hand side 433 434 if (parens[0]) { 435 // left hand side in parenthesis? 436 lhs = '(' + lhs + ')'; 437 } 438 439 if (parens[1]) { 440 // right hand side in parenthesis? 441 rhs = '(' + rhs + ')'; 442 } 443 444 if (this.implicit && this.getIdentifier() === 'OperatorNode:multiply' && implicit === 'hide') { 445 return lhs + ' ' + rhs; 446 } 447 448 return lhs + ' ' + this.op + ' ' + rhs; 449 } else if (args.length > 2 && (this.getIdentifier() === 'OperatorNode:add' || this.getIdentifier() === 'OperatorNode:multiply')) { 450 var stringifiedArgs = args.map(function (arg, index) { 451 arg = arg.toString(options); 452 453 if (parens[index]) { 454 // put in parenthesis? 455 arg = '(' + arg + ')'; 456 } 457 458 return arg; 459 }); 460 461 if (this.implicit && this.getIdentifier() === 'OperatorNode:multiply' && implicit === 'hide') { 462 return stringifiedArgs.join(' '); 463 } 464 465 return stringifiedArgs.join(' ' + this.op + ' '); 466 } else { 467 // fallback to formatting as a function call 468 return this.fn + '(' + this.args.join(', ') + ')'; 469 } 470 }; 471 /** 472 * Get a JSON representation of the node 473 * @returns {Object} 474 */ 475 476 477 OperatorNode.prototype.toJSON = function () { 478 return { 479 mathjs: 'OperatorNode', 480 op: this.op, 481 fn: this.fn, 482 args: this.args, 483 implicit: this.implicit, 484 isPercentage: this.isPercentage 485 }; 486 }; 487 /** 488 * Instantiate an OperatorNode from its JSON representation 489 * @param {Object} json An object structured like 490 * `{"mathjs": "OperatorNode", "op": "+", "fn": "add", "args": [...], "implicit": false, "isPercentage":false}`, 491 * where mathjs is optional 492 * @returns {OperatorNode} 493 */ 494 495 496 OperatorNode.fromJSON = function (json) { 497 return new OperatorNode(json.op, json.fn, json.args, json.implicit, json.isPercentage); 498 }; 499 /** 500 * Get HTML representation. 501 * @param {Object} options 502 * @return {string} str 503 */ 504 505 506 OperatorNode.prototype.toHTML = function (options) { 507 var parenthesis = options && options.parenthesis ? options.parenthesis : 'keep'; 508 var implicit = options && options.implicit ? options.implicit : 'hide'; 509 var args = this.args; 510 var parens = calculateNecessaryParentheses(this, parenthesis, implicit, args, false); 511 512 if (args.length === 1) { 513 // unary operators 514 var assoc = (0, _operators.getAssociativity)(this, parenthesis); 515 var operand = args[0].toHTML(options); 516 517 if (parens[0]) { 518 operand = '<span class="math-parenthesis math-round-parenthesis">(</span>' + operand + '<span class="math-parenthesis math-round-parenthesis">)</span>'; 519 } 520 521 if (assoc === 'right') { 522 // prefix operator 523 return '<span class="math-operator math-unary-operator math-lefthand-unary-operator">' + (0, _string.escape)(this.op) + '</span>' + operand; 524 } else { 525 // postfix when assoc === 'left' or undefined 526 return operand + '<span class="math-operator math-unary-operator math-righthand-unary-operator">' + (0, _string.escape)(this.op) + '</span>'; 527 } 528 } else if (args.length === 2) { 529 // binary operatoes 530 var lhs = args[0].toHTML(options); // left hand side 531 532 var rhs = args[1].toHTML(options); // right hand side 533 534 if (parens[0]) { 535 // left hand side in parenthesis? 536 lhs = '<span class="math-parenthesis math-round-parenthesis">(</span>' + lhs + '<span class="math-parenthesis math-round-parenthesis">)</span>'; 537 } 538 539 if (parens[1]) { 540 // right hand side in parenthesis? 541 rhs = '<span class="math-parenthesis math-round-parenthesis">(</span>' + rhs + '<span class="math-parenthesis math-round-parenthesis">)</span>'; 542 } 543 544 if (this.implicit && this.getIdentifier() === 'OperatorNode:multiply' && implicit === 'hide') { 545 return lhs + '<span class="math-operator math-binary-operator math-implicit-binary-operator"></span>' + rhs; 546 } 547 548 return lhs + '<span class="math-operator math-binary-operator math-explicit-binary-operator">' + (0, _string.escape)(this.op) + '</span>' + rhs; 549 } else { 550 var stringifiedArgs = args.map(function (arg, index) { 551 arg = arg.toHTML(options); 552 553 if (parens[index]) { 554 // put in parenthesis? 555 arg = '<span class="math-parenthesis math-round-parenthesis">(</span>' + arg + '<span class="math-parenthesis math-round-parenthesis">)</span>'; 556 } 557 558 return arg; 559 }); 560 561 if (args.length > 2 && (this.getIdentifier() === 'OperatorNode:add' || this.getIdentifier() === 'OperatorNode:multiply')) { 562 if (this.implicit && this.getIdentifier() === 'OperatorNode:multiply' && implicit === 'hide') { 563 return stringifiedArgs.join('<span class="math-operator math-binary-operator math-implicit-binary-operator"></span>'); 564 } 565 566 return stringifiedArgs.join('<span class="math-operator math-binary-operator math-explicit-binary-operator">' + (0, _string.escape)(this.op) + '</span>'); 567 } else { 568 // fallback to formatting as a function call 569 return '<span class="math-function">' + (0, _string.escape)(this.fn) + '</span><span class="math-paranthesis math-round-parenthesis">(</span>' + stringifiedArgs.join('<span class="math-separator">,</span>') + '<span class="math-paranthesis math-round-parenthesis">)</span>'; 570 } 571 } 572 }; 573 /** 574 * Get LaTeX representation 575 * @param {Object} options 576 * @return {string} str 577 */ 578 579 580 OperatorNode.prototype._toTex = function (options) { 581 var parenthesis = options && options.parenthesis ? options.parenthesis : 'keep'; 582 var implicit = options && options.implicit ? options.implicit : 'hide'; 583 var args = this.args; 584 var parens = calculateNecessaryParentheses(this, parenthesis, implicit, args, true); 585 var op = _latex.latexOperators[this.fn]; 586 op = typeof op === 'undefined' ? this.op : op; // fall back to using this.op 587 588 if (args.length === 1) { 589 // unary operators 590 var assoc = (0, _operators.getAssociativity)(this, parenthesis); 591 var operand = args[0].toTex(options); 592 593 if (parens[0]) { 594 operand = "\\left(".concat(operand, "\\right)"); 595 } 596 597 if (assoc === 'right') { 598 // prefix operator 599 return op + operand; 600 } else if (assoc === 'left') { 601 // postfix operator 602 return operand + op; 603 } // fall back to postfix 604 605 606 return operand + op; 607 } else if (args.length === 2) { 608 // binary operators 609 var lhs = args[0]; // left hand side 610 611 var lhsTex = lhs.toTex(options); 612 613 if (parens[0]) { 614 lhsTex = "\\left(".concat(lhsTex, "\\right)"); 615 } 616 617 var rhs = args[1]; // right hand side 618 619 var rhsTex = rhs.toTex(options); 620 621 if (parens[1]) { 622 rhsTex = "\\left(".concat(rhsTex, "\\right)"); 623 } // handle some exceptions (due to the way LaTeX works) 624 625 626 var lhsIdentifier; 627 628 if (parenthesis === 'keep') { 629 lhsIdentifier = lhs.getIdentifier(); 630 } else { 631 // Ignore ParenthesisNodes if in 'keep' mode 632 lhsIdentifier = lhs.getContent().getIdentifier(); 633 } 634 635 switch (this.getIdentifier()) { 636 case 'OperatorNode:divide': 637 // op contains '\\frac' at this point 638 return op + '{' + lhsTex + '}' + '{' + rhsTex + '}'; 639 640 case 'OperatorNode:pow': 641 lhsTex = '{' + lhsTex + '}'; 642 rhsTex = '{' + rhsTex + '}'; 643 644 switch (lhsIdentifier) { 645 case 'ConditionalNode': // 646 647 case 'OperatorNode:divide': 648 lhsTex = "\\left(".concat(lhsTex, "\\right)"); 649 } 650 651 break; 652 653 case 'OperatorNode:multiply': 654 if (this.implicit && implicit === 'hide') { 655 return lhsTex + '~' + rhsTex; 656 } 657 658 } 659 660 return lhsTex + op + rhsTex; 661 } else if (args.length > 2 && (this.getIdentifier() === 'OperatorNode:add' || this.getIdentifier() === 'OperatorNode:multiply')) { 662 var texifiedArgs = args.map(function (arg, index) { 663 arg = arg.toTex(options); 664 665 if (parens[index]) { 666 arg = "\\left(".concat(arg, "\\right)"); 667 } 668 669 return arg; 670 }); 671 672 if (this.getIdentifier() === 'OperatorNode:multiply' && this.implicit) { 673 return texifiedArgs.join('~'); 674 } 675 676 return texifiedArgs.join(op); 677 } else { 678 // fall back to formatting as a function call 679 // as this is a fallback, it doesn't use 680 // fancy function names 681 return '\\mathrm{' + this.fn + '}\\left(' + args.map(function (arg) { 682 return arg.toTex(options); 683 }).join(',') + '\\right)'; 684 } 685 }; 686 /** 687 * Get identifier. 688 * @return {string} 689 */ 690 691 692 OperatorNode.prototype.getIdentifier = function () { 693 return this.type + ':' + this.fn; 694 }; 695 696 return OperatorNode; 697 }, { 698 isClass: true, 699 isNode: true 700 }); 701 exports.createOperatorNode = createOperatorNode;