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