UNPKG

13.1 kBJavaScriptView Raw
1import { isNode } from '../../utils/is'
2
3import { keywords } from '../keywords'
4import { deepStrictEqual, hasOwnProperty } from '../../utils/object'
5import { factory } from '../../utils/factory'
6import { warnOnce } from '../../utils/log'
7
8const name = 'Node'
9const dependencies = ['mathWithTransform']
10
11export const createNode = /* #__PURE__ */ factory(name, dependencies, ({ mathWithTransform }) => {
12 /**
13 * Node
14 */
15 function Node () {
16 if (!(this instanceof Node)) {
17 throw new SyntaxError('Constructor must be called with the new operator')
18 }
19 }
20
21 /**
22 * Evaluate the node
23 * @param {Object} [scope] Scope to read/write variables
24 * @return {*} Returns the result
25 */
26 Node.prototype.evaluate = function (scope) {
27 return this.compile().evaluate(scope)
28 }
29
30 /**
31 * Evaluate the node
32 * @param {Object} [scope] Scope to read/write variables
33 * @return {*} Returns the result
34 */
35 // TODO: Deprecated since v6.0.0. Clean up some day
36 Node.prototype.eval = function (scope) {
37 warnOnce('Method Node.eval is renamed to Node.evaluate. Please use the new method name.')
38
39 return this.evaluate(scope)
40 }
41
42 Node.prototype.type = 'Node'
43
44 Node.prototype.isNode = true
45
46 Node.prototype.comment = ''
47
48 /**
49 * Compile the node into an optimized, evauatable JavaScript function
50 * @return {{evaluate: function([Object])}} object
51 * Returns an object with a function 'evaluate',
52 * which can be invoked as expr.evaluate([scope: Object]),
53 * where scope is an optional object with
54 * variables.
55 */
56 Node.prototype.compile = function () {
57 const expr = this._compile(mathWithTransform, {})
58 const args = {}
59 const context = null
60
61 function evaluate (scope) {
62 const s = scope || {}
63 _validateScope(s)
64 return expr(s, args, context)
65 }
66
67 return {
68 evaluate,
69
70 // TODO: Deprecated since v6.0.0. Clean up some day
71 eval: function deprecatedEval (scope) {
72 warnOnce('Method eval is renamed to evaluate. Please use the new method.')
73
74 return evaluate(scope)
75 }
76 }
77 }
78
79 /**
80 * Compile a node into a JavaScript function.
81 * This basically pre-calculates as much as possible and only leaves open
82 * calculations which depend on a dynamic scope with variables.
83 * @param {Object} math Math.js namespace with functions and constants.
84 * @param {Object} argNames An object with argument names as key and `true`
85 * as value. Used in the SymbolNode to optimize
86 * for arguments from user assigned functions
87 * (see FunctionAssignmentNode) or special symbols
88 * like `end` (see IndexNode).
89 * @return {function} Returns a function which can be called like:
90 * evalNode(scope: Object, args: Object, context: *)
91 */
92 Node.prototype._compile = function (math, argNames) {
93 throw new Error('Method _compile should be implemented by type ' + this.type)
94 }
95
96 /**
97 * Execute a callback for each of the child nodes of this node
98 * @param {function(child: Node, path: string, parent: Node)} callback
99 */
100 Node.prototype.forEach = function (callback) {
101 // must be implemented by each of the Node implementations
102 throw new Error('Cannot run forEach on a Node interface')
103 }
104
105 /**
106 * Create a new Node having it's childs be the results of calling
107 * the provided callback function for each of the childs of the original node.
108 * @param {function(child: Node, path: string, parent: Node): Node} callback
109 * @returns {OperatorNode} Returns a transformed copy of the node
110 */
111 Node.prototype.map = function (callback) {
112 // must be implemented by each of the Node implementations
113 throw new Error('Cannot run map on a Node interface')
114 }
115
116 /**
117 * Validate whether an object is a Node, for use with map
118 * @param {Node} node
119 * @returns {Node} Returns the input if it's a node, else throws an Error
120 * @protected
121 */
122 Node.prototype._ifNode = function (node) {
123 if (!isNode(node)) {
124 throw new TypeError('Callback function must return a Node')
125 }
126
127 return node
128 }
129
130 /**
131 * Recursively traverse all nodes in a node tree. Executes given callback for
132 * this node and each of its child nodes.
133 * @param {function(node: Node, path: string, parent: Node)} callback
134 * A callback called for every node in the node tree.
135 */
136 Node.prototype.traverse = function (callback) {
137 // execute callback for itself
138 callback(this, null, null) // eslint-disable-line standard/no-callback-literal
139
140 // recursively traverse over all childs of a node
141 function _traverse (node, callback) {
142 node.forEach(function (child, path, parent) {
143 callback(child, path, parent)
144 _traverse(child, callback)
145 })
146 }
147
148 _traverse(this, callback)
149 }
150
151 /**
152 * Recursively transform a node tree via a transform function.
153 *
154 * For example, to replace all nodes of type SymbolNode having name 'x' with a
155 * ConstantNode with value 2:
156 *
157 * const res = Node.transform(function (node, path, parent) {
158 * if (node && node.isSymbolNode) && (node.name === 'x')) {
159 * return new ConstantNode(2)
160 * }
161 * else {
162 * return node
163 * }
164 * })
165 *
166 * @param {function(node: Node, path: string, parent: Node) : Node} callback
167 * A mapping function accepting a node, and returning
168 * a replacement for the node or the original node.
169 * Signature: callback(node: Node, index: string, parent: Node) : Node
170 * @return {Node} Returns the original node or its replacement
171 */
172 Node.prototype.transform = function (callback) {
173 function _transform (child, path, parent) {
174 const replacement = callback(child, path, parent)
175
176 if (replacement !== child) {
177 // stop iterating when the node is replaced
178 return replacement
179 }
180
181 return child.map(_transform)
182 }
183
184 return _transform(this, null, null)
185 }
186
187 /**
188 * Find any node in the node tree matching given filter function. For example, to
189 * find all nodes of type SymbolNode having name 'x':
190 *
191 * const results = Node.filter(function (node) {
192 * return (node && node.isSymbolNode) && (node.name === 'x')
193 * })
194 *
195 * @param {function(node: Node, path: string, parent: Node) : Node} callback
196 * A test function returning true when a node matches, and false
197 * otherwise. Function signature:
198 * callback(node: Node, index: string, parent: Node) : boolean
199 * @return {Node[]} nodes An array with nodes matching given filter criteria
200 */
201 Node.prototype.filter = function (callback) {
202 const nodes = []
203
204 this.traverse(function (node, path, parent) {
205 if (callback(node, path, parent)) {
206 nodes.push(node)
207 }
208 })
209
210 return nodes
211 }
212
213 // TODO: deprecated since version 1.1.0, remove this some day
214 Node.prototype.find = function () {
215 throw new Error('Function Node.find is deprecated. Use Node.filter instead.')
216 }
217
218 // TODO: deprecated since version 1.1.0, remove this some day
219 Node.prototype.match = function () {
220 throw new Error('Function Node.match is deprecated. See functions Node.filter, Node.transform, Node.traverse.')
221 }
222
223 /**
224 * Create a shallow clone of this node
225 * @return {Node}
226 */
227 Node.prototype.clone = function () {
228 // must be implemented by each of the Node implementations
229 throw new Error('Cannot clone a Node interface')
230 }
231
232 /**
233 * Create a deep clone of this node
234 * @return {Node}
235 */
236 Node.prototype.cloneDeep = function () {
237 return this.map(function (node) {
238 return node.cloneDeep()
239 })
240 }
241
242 /**
243 * Deep compare this node with another node.
244 * @param {Node} other
245 * @return {boolean} Returns true when both nodes are of the same type and
246 * contain the same values (as do their childs)
247 */
248 Node.prototype.equals = function (other) {
249 return other
250 ? deepStrictEqual(this, other)
251 : false
252 }
253
254 /**
255 * Get string representation. (wrapper function)
256 *
257 * This function can get an object of the following form:
258 * {
259 * handler: //This can be a callback function of the form
260 * // "function callback(node, options)"or
261 * // a map that maps function names (used in FunctionNodes)
262 * // to callbacks
263 * parenthesis: "keep" //the parenthesis option (This is optional)
264 * }
265 *
266 * @param {Object} [options]
267 * @return {string}
268 */
269 Node.prototype.toString = function (options) {
270 let customString
271 if (options && typeof options === 'object') {
272 switch (typeof options.handler) {
273 case 'object':
274 case 'undefined':
275 break
276 case 'function':
277 customString = options.handler(this, options)
278 break
279 default:
280 throw new TypeError('Object or function expected as callback')
281 }
282 }
283
284 if (typeof customString !== 'undefined') {
285 return customString
286 }
287
288 return this._toString(options)
289 }
290
291 /**
292 * Get a JSON representation of the node
293 * Both .toJSON() and the static .fromJSON(json) should be implemented by all
294 * implementations of Node
295 * @returns {Object}
296 */
297 Node.prototype.toJSON = function () {
298 throw new Error('Cannot serialize object: toJSON not implemented by ' + this.type)
299 }
300
301 /**
302 * Get HTML representation. (wrapper function)
303 *
304 * This function can get an object of the following form:
305 * {
306 * handler: //This can be a callback function of the form
307 * // "function callback(node, options)" or
308 * // a map that maps function names (used in FunctionNodes)
309 * // to callbacks
310 * parenthesis: "keep" //the parenthesis option (This is optional)
311 * }
312 *
313 * @param {Object} [options]
314 * @return {string}
315 */
316 Node.prototype.toHTML = function (options) {
317 let customString
318 if (options && typeof options === 'object') {
319 switch (typeof options.handler) {
320 case 'object':
321 case 'undefined':
322 break
323 case 'function':
324 customString = options.handler(this, options)
325 break
326 default:
327 throw new TypeError('Object or function expected as callback')
328 }
329 }
330
331 if (typeof customString !== 'undefined') {
332 return customString
333 }
334
335 return this.toHTML(options)
336 }
337
338 /**
339 * Internal function to generate the string output.
340 * This has to be implemented by every Node
341 *
342 * @throws {Error}
343 */
344 Node.prototype._toString = function () {
345 // must be implemented by each of the Node implementations
346 throw new Error('_toString not implemented for ' + this.type)
347 }
348
349 /**
350 * Get LaTeX representation. (wrapper function)
351 *
352 * This function can get an object of the following form:
353 * {
354 * handler: //This can be a callback function of the form
355 * // "function callback(node, options)"or
356 * // a map that maps function names (used in FunctionNodes)
357 * // to callbacks
358 * parenthesis: "keep" //the parenthesis option (This is optional)
359 * }
360 *
361 * @param {Object} [options]
362 * @return {string}
363 */
364 Node.prototype.toTex = function (options) {
365 let customTex
366 if (options && typeof options === 'object') {
367 switch (typeof options.handler) {
368 case 'object':
369 case 'undefined':
370 break
371 case 'function':
372 customTex = options.handler(this, options)
373 break
374 default:
375 throw new TypeError('Object or function expected as callback')
376 }
377 }
378
379 if (typeof customTex !== 'undefined') {
380 return customTex
381 }
382
383 return this._toTex(options)
384 }
385
386 /**
387 * Internal function to generate the LaTeX output.
388 * This has to be implemented by every Node
389 *
390 * @param {Object} [options]
391 * @throws {Error}
392 */
393 Node.prototype._toTex = function (options) {
394 // must be implemented by each of the Node implementations
395 throw new Error('_toTex not implemented for ' + this.type)
396 }
397
398 /**
399 * Get identifier.
400 * @return {string}
401 */
402 Node.prototype.getIdentifier = function () {
403 return this.type
404 }
405
406 /**
407 * Get the content of the current Node.
408 * @return {Node} node
409 **/
410 Node.prototype.getContent = function () {
411 return this
412 }
413
414 /**
415 * Validate the symbol names of a scope.
416 * Throws an error when the scope contains an illegal symbol.
417 * @param {Object} scope
418 */
419 function _validateScope (scope) {
420 for (const symbol in scope) {
421 if (hasOwnProperty(scope, symbol)) {
422 if (symbol in keywords) {
423 throw new Error('Scope contains an illegal symbol, "' + symbol + '" is a reserved keyword')
424 }
425 }
426 }
427 }
428
429 return Node
430}, { isClass: true, isNode: true })