Max Bruckner a973482ef2 Fix bug in fix of breaking change
Without this fix, the type passed to ArrayNode.toTex would stay there
forever until you change it back manually instead of being used only
once.
2015-03-14 09:09:07 +01:00

339 lines
9.7 KiB
JavaScript

'use strict';
var keywords = require('../keywords');
/**
* Node
*/
function Node() {
if (!(this instanceof Node)) {
throw new SyntaxError('Constructor must be called with the new operator');
}
}
/**
* Evaluate the node
* @return {*} result
*/
// TODO: cleanup deprecated code one day. Deprecated since version 0.19.0
Node.prototype.eval = function () {
throw new Error('Node.eval is deprecated. ' +
'Use Node.compile(math).eval([scope]) instead.');
};
Node.prototype.type = 'Node';
/**
* Compile the node to javascript code
* @param {Object} math math.js instance
* @return {{eval: function}} expr Returns an object with a function 'eval',
* which can be invoked as expr.eval([scope]),
* where scope is an optional object with
* variables.
*/
Node.prototype.compile = function (math) {
if (!(math instanceof Object)) {
throw new TypeError('Object expected for parameter math');
}
// definitions globally available inside the closure of the compiled expressions
var defs = {
math: _transform(math),
_validateScope: _validateScope
};
var code = this._compile(defs);
var defsCode = Object.keys(defs).map(function (name) {
return ' var ' + name + ' = defs["' + name + '"];';
});
var factoryCode =
defsCode.join(' ') +
'return {' +
' "eval": function (scope) {' +
' if (scope) _validateScope(scope);' +
' scope = scope || {};' +
' return ' + code + ';' +
' }' +
'};';
var factory = new Function ('defs', factoryCode);
return factory(defs);
};
/**
* Compile the node to javascript code
* @param {Object} defs Object which can be used to define functions
* and constants globally available inside the closure
* of the compiled expression
* @return {String} js
* @private
*/
Node.prototype._compile = function (defs) {
// must be implemented by each of the Node implementations
throw new Error('Cannot compile a Node interface');
};
/**
* Execute a callback for each of the child nodes of this node
* @param {function(child: Node, path: string, parent: Node)} callback
*/
Node.prototype.forEach = function (callback) {
// must be implemented by each of the Node implementations
throw new Error('Cannot run forEach on a Node interface');
};
/**
* Create a new Node having it's childs be the results of calling
* the provided callback function for each of the childs of the original node.
* @param {function(child: Node, path: string, parent: Node): Node} callback
* @returns {OperatorNode} Returns a transformed copy of the node
*/
Node.prototype.map = function (callback) {
// must be implemented by each of the Node implementations
throw new Error('Cannot run map on a Node interface');
};
/**
* Validate whether an object is a Node, for use with map
* @param {Node} node
* @returns {Node} Returns the input if it's a node, else throws an Error
* @protected
*/
Node.prototype._ifNode = function (node) {
if (!(node instanceof Node)) {
throw new TypeError('Callback function must return a Node');
}
return node;
};
/**
* Recursively traverse all nodes in a node tree. Executes given callback for
* this node and each of its child nodes.
* @param {function(node: Node, path: string, parent: Node)} callback
* A callback called for every node in the node tree.
*/
Node.prototype.traverse = function (callback) {
// execute callback for itself
callback(this, null, null);
// recursively traverse over all childs of a node
function _traverse (node, callback) {
node.forEach(function(child, path, parent) {
callback(child, path, parent);
_traverse(child, callback);
});
}
_traverse(this, callback);
};
/**
* Recursively transform a node tree via a transform function.
*
* For example, to replace all nodes of type SymbolNode having name 'x' with a
* ConstantNode with value 2:
*
* var res = Node.transform(function (node, path, parent) {
* if (node instanceof SymbolNode) && (node.name == 'x')) {
* return new ConstantNode(2);
* }
* else {
* return node;
* }
* });
*
* @param {function(node: Node, path: string, parent: Node) : Node} callback
* A mapping function accepting a node, and returning
* a replacement for the node or the original node.
* Signature: callback(node: Node, index: string, parent: Node) : Node
* @return {Node} Returns the original node or its replacement
*/
Node.prototype.transform = function (callback) {
// check itself
var replacement = callback(this, null, null);
if (replacement !== this) {
return replacement;
}
// traverse over all childs
function _transform (node, callback) {
return node.map(function(child, path, parent) {
var replacement = callback(child, path, parent);
return (replacement !== child) ? replacement : _transform(child, callback);
});
}
return _transform(this, callback);
};
/**
* Find any node in the node tree matching given filter function. For example, to
* find all nodes of type SymbolNode having name 'x':
*
* var results = Node.filter(function (node) {
* return (node instanceof SymbolNode) && (node.name == 'x');
* });
*
* @param {function(node: Node, path: string, parent: Node) : Node} callback
* A test function returning true when a node matches, and false
* otherwise. Function signature:
* callback(node: Node, index: string, parent: Node) : boolean
* @return {Node[]} nodes An array with nodes matching given filter criteria
*/
Node.prototype.filter = function (callback) {
var nodes = [];
this.traverse(function (node, path, parent) {
if (callback(node, path, parent)) {
nodes.push(node);
}
});
return nodes;
};
// TODO: deprecated since version 1.1.0, remove this some day
Node.prototype.find = function () {
throw new Error('Function Node.find is deprecated. Use Node.filter instead.');
};
// TODO: deprecated since version 1.1.0, remove this some day
Node.prototype.match = function () {
throw new Error('Function Node.match is deprecated. See functions Node.filter, Node.transform, Node.traverse.');
};
/**
* Create a clone of this node, a shallow copy
* @return {Node}
*/
Node.prototype.clone = function() {
// must be implemented by each of the Node implementations
throw new Error('Cannot clone a Node interface');
};
/**
* Get string representation
* @return {String}
*/
Node.prototype.toString = function() {
return '';
};
/**
* Get LaTeX representation. (wrapper function)
* This functions get's either an object containing callbacks or
* a single callback. It decides whether to call the callback and if
* not or if the callback returns nothing, it calls the default
* LaTeX implementation of the node (_toTex).
*
* @param {Object|function} callback(s)
* @return {String}
*/
Node.prototype.toTex = function(customFunctions) {
var customTex;
if (this.type === 'ArrayNode') {
//FIXME this is only a workaround for a breaking change,
//remove this in version2
delete this.latexType;
}
if (typeof customFunctions === 'object') {
//if customFunctions is a map of callback functions
if (customFunctions.hasOwnProperty(this.type)) {
customTex = customFunctions[this.type](this, customFunctions);
}
else if (customFunctions.hasOwnProperty(this.getIdentifier())) {
customTex = customFunctions[this.getIdentifier()](this, customFunctions);
}
}
else if (typeof customFunctions === 'function') {
//if customFunctions is a callback function
customTex = customFunctions(this, customFunctions);
}
else if ((typeof customFunctions === 'string') && (this.type === 'ArrayNode')) {
//FIXME this is only a workaround for a breaking change,
//remove this in version2
this.latexType = customFunctions;
}
else if (typeof customFunctions !== 'undefined') {
throw new TypeError('Object or function expected as callback');
}
if (typeof customTex !== 'undefined') {
return customTex;
}
return this._toTex(customFunctions);
};
/**
* Internal function to generate the LaTeX output.
* This has to be implemented by every Node
*
* @param {Object}|function}
* @throws {Error}
*/
Node.prototype._toTex = function () {
if (this.type === 'Node') {
//FIXME remove this in v2
return '';
}
//must be implemented by each of the Node implementations
throw new Error('_toTex not implemented for this Node');
};
/**
* Get identifier.
* @return {String}
*/
Node.prototype.getIdentifier = function () {
return this.type;
};
/**
* Test whether an object is a Node
* @param {*} object
* @returns {boolean} isNode
*/
Node.isNode = function(object) {
return object instanceof Node;
};
/**
* Validate the symbol names of a scope.
* Throws an error when the scope contains an illegal symbol.
* @param {Object} scope
*/
function _validateScope (scope) {
for (var symbol in scope) {
if (scope.hasOwnProperty(symbol)) {
if (symbol in keywords) {
throw new Error('Scope contains an illegal symbol, "' + symbol + '" is a reserved keyword');
}
}
}
}
/**
* Replace all functions having a transform function attached at property transform
* with their transform.
* @param {Object} math
* @return {Object} transformed
* @private
*/
function _transform(math) {
var transformed = Object.create(math);
if (math.expression && math.expression.transform) {
for (var name in math.expression.transform) {
if (math.expression.transform.hasOwnProperty(name)) {
transformed[name] = math.expression.transform[name];
}
}
}
return transformed;
}
module.exports = Node;