UNPKG

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