UNPKG

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