UNPKG

12.6 kBJavaScriptView Raw
1'use strict'
2
3const keywords = require('../keywords')
4const deepEqual = require('../../utils/object').deepEqual
5const hasOwnProperty = require('../../utils/object').hasOwnProperty
6
7function 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
403exports.name = 'Node'
404exports.path = 'expression.node'
405exports.math = true // request access to the math namespace as 5th argument of the factory function
406exports.factory = factory