UNPKG

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