1 | import { isNode } from '../../utils/is'
|
2 |
|
3 | import { keywords } from '../keywords'
|
4 | import { deepStrictEqual, hasOwnProperty } from '../../utils/object'
|
5 | import { factory } from '../../utils/factory'
|
6 | import { warnOnce } from '../../utils/log'
|
7 |
|
8 | const name = 'Node'
|
9 | const dependencies = ['mathWithTransform']
|
10 |
|
11 | export 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 })
|