UNPKG

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