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