Repository: josdejong/mathjs
Pull request #301 (2015-03-13 20:27:53)

custom toTex
This is a new pull request, continuing the effort in #298.
I had to make a new pull request because github got confused by my rebases.

This version simplifies the implementation while incorporating your suggestions *and* keeping my solution of passing a map of callbacks ( take a look at the documentation included in this pull request for more details ).

This is ready for review!

About passing a map of callbacks:
1. It allows having multiple maps and merging them together ( you can easily combine callbacks from several sources ).
2. It's a more intuitive UI in my opinion.
3. It's still possible to just pass a single callback if you want to.
4. It's only 9 lines of code and it can be easily replaced by a better solution in v2.

**Update:** I fixed my fix ( see commit message ). ( and the problem with github displaying commits in the wrong order seems to be fixed )


Patch:
diff --git a/docs/expressions/customization.md b/docs/expressions/customization.md
index 245ba7df7..543fb4944 100644
--- a/docs/expressions/customization.md
+++ b/docs/expressions/customization.md
@@ -139,3 +139,95 @@ math.import({
 math.eval('myFunction(2 + 3, sqrt(4))');
 // returns 'arguments: 2 + 3, sqrt(4), evaluated: 5, 2'
 ```
+
+## Custom LaTeX conversion
+
+You can provide the `toTex` function of an expression tree with your own LaTeX converters.
+This can be used to override the builtin LaTeX conversion or provide LaTeX output for your own custom functions.
+
+You can pass your own callback(s) to `toTex`. If it returns nothing, the standard LaTeX conversion will be use.
+If your callback returns a string, this string will be used.
+
+There's two ways of passing callbacks:
+1. Pass an object that maps function names to callbacks. Those callbacks will be used for FunctionNodes with 
+functions of that name.
+2. Pass a function to `toTex`. This function will then be used for every node.
+
+
+**Examples for option 2**
+```js
+var customFunctions = {
+  binomial: function (n, k) {
+    //calculate n choose k
+    // (do some stuff)
+    return result;
+  }
+};
+
+var customLaTeX = {
+  'binomial': function (node, callbacks) { //provide toTex for your own custom function
+    return '\\binom{' + node.args[0].toTex(callbacks) + '}{' + node.args[1].toTex(callbacks) + '}';
+  },
+  'factorial': function (node, callbacks) { //override toTex for builtin functions
+  	return 'factorial\\left(' + node.args[0] + '\\right)';
+  }
+};
+```
+
+You can simply use your custom toTex functions by passing them to `toTex`:
+```js
+math.import(customFunctions);
+var expression = math.parse('binomial(factorial(2),1)');
+var latex = expression.toTex(customLaTeX);
+//latex now contains "\binom{factorial\\left(2\\right)}{1}"
+```
+
+**Examples for option 2:**
+
+```js
+var customLaTeX = function (node, callback) {
+  if ((node.type === 'OperatorNode') && (node.fn === 'add')) {
+    //don't forget to pass the callback to the toTex functions
+    return node.args[0].toTex(callback) + ' plus ' + node.args[1].toTex(callback);
+  }
+  else if (node.type === 'ConstantNode') {
+    if (node.value == 0) {
+        return '\\mbox{zero}';
+    }
+    else if (node.value == 1) {
+        return '\\mbox{one}';
+    }
+    else if (node.value == 2) {
+        return '\\mbox{two}';
+    }
+    else {
+        return node.value;
+    }
+  }
+};
+
+var expression = math.parse('1+2');
+var latex = expression.toTex(customLaTeX);
+//latex now contains '\mbox{one} plus \mbox{two}'
+```
+Another example in conjunction with custom functions:
+```js
+var customFunctions = {
+  binomial: function (n, k) {
+    //calculate n choose k
+    // (do some stuff)
+    return result;
+  }
+};
+
+var customLaTeX = function (node, callback) {
+  if ((node.type === 'FunctionNode') && (node.name === 'binomial')) {
+      return '\\binom{' + node.args[0].toTex(callback) + '}{' + node.args[1].toTex(callback) + '}';
+  }
+};
+
+math.import(customFunctions);
+var expression = math.parse('binomial(2,1)');
+var latex = expression.toTex(customLaTeX);
+//latex now contains "\binom{2}{1}"
+```
diff --git a/lib/expression/node/ArrayNode.js b/lib/expression/node/ArrayNode.js
index fbbb67bf7..c4636c06c 100644
--- a/lib/expression/node/ArrayNode.js
+++ b/lib/expression/node/ArrayNode.js
@@ -95,26 +95,28 @@ ArrayNode.prototype.toString = function() {
 
 /**
  * Get LaTeX representation
+ * @param {Object|function} callback(s)
+ * @param {String} type
  * @return {String} str
  */
-ArrayNode.prototype.toTex = function(type) {
-  type = type || 'bmatrix';
-  var s = '\\begin{' + type + '}';
+ArrayNode.prototype._toTex = function(callbacks) {
+  this.latexType = this.latexType || 'bmatrix';
+  var s = '\\begin{' + this.latexType + '}';
 
   this.nodes.forEach(function(node) {
     if (node.nodes) {
       s += node.nodes.map(function(childNode) {
-        return childNode.toTex();
+        return childNode.toTex(callbacks);
       }).join('&');
     }
     else {
-      s += node.toTex();
+      s += node.toTex(callbacks);
     }
 
     // new line
     s += '\\\\';
   });
-  s += '\\end{' + type + '}';
+  s += '\\end{' + this.latexType + '}';
   return s;
 };
 
diff --git a/lib/expression/node/AssignmentNode.js b/lib/expression/node/AssignmentNode.js
index 90fb93373..e47e22349 100644
--- a/lib/expression/node/AssignmentNode.js
+++ b/lib/expression/node/AssignmentNode.js
@@ -89,13 +89,14 @@ AssignmentNode.prototype.toString = function() {
 
 /**
  * Get LaTeX representation
+ * @param {Object|function} callback(s)
  * @return {String}
  */
-AssignmentNode.prototype.toTex = function() {
+AssignmentNode.prototype._toTex = function(callbacks) {
   var precedence = operators.getPrecedence(this);
   var exprPrecedence = operators.getPrecedence(this.expr);
 
-  var expr = this.expr.toTex();
+  var expr = this.expr.toTex(callbacks);
   if ((exprPrecedence !== null) && (exprPrecedence <= precedence)) {
     //adds visible round brackets
     expr = latex.addBraces(expr, true);
diff --git a/lib/expression/node/BlockNode.js b/lib/expression/node/BlockNode.js
index 57c2e4e03..3bccddbd1 100644
--- a/lib/expression/node/BlockNode.js
+++ b/lib/expression/node/BlockNode.js
@@ -122,11 +122,12 @@ BlockNode.prototype.toString = function() {
 
 /**
  * Get LaTeX representation
+ * @param {Object|function} callback(s)
  * @return {String} str
  */
-BlockNode.prototype.toTex = function() {
+BlockNode.prototype._toTex = function(callbacks) {
   return this.blocks.map(function (param) {
-    return param.node.toTex() + (param.visible ? '' : ';');
+    return param.node.toTex(callbacks) + (param.visible ? '' : ';');
   }).join('\n');
 };
 
diff --git a/lib/expression/node/ConditionalNode.js b/lib/expression/node/ConditionalNode.js
index 54ea5c02f..e5acb04a5 100644
--- a/lib/expression/node/ConditionalNode.js
+++ b/lib/expression/node/ConditionalNode.js
@@ -151,15 +151,16 @@ ConditionalNode.prototype.toString = function() {
 
 /**
  * Get LaTeX representation
+ * @param {Object|function} callback(s)
  * @return {String} str
  */
-ConditionalNode.prototype.toTex = function() {
+ConditionalNode.prototype._toTex = function(callbacks) {
   var s = (
-      latex.addBraces(this.trueExpr.toTex()) +
+      latex.addBraces(this.trueExpr.toTex(callbacks)) +
       ', &\\quad' +
-      latex.addBraces('\\text{if}\\;' + this.condition.toTex())
+      latex.addBraces('\\text{if}\\;' + this.condition.toTex(callbacks))
       ) + '\\\\' + (
-      latex.addBraces(this.falseExpr.toTex()) +
+      latex.addBraces(this.falseExpr.toTex(callbacks)) +
       ', &\\quad' +
       latex.addBraces('\\text{otherwise}')
       );
diff --git a/lib/expression/node/ConstantNode.js b/lib/expression/node/ConstantNode.js
index 5b47d40e8..10e8448cf 100644
--- a/lib/expression/node/ConstantNode.js
+++ b/lib/expression/node/ConstantNode.js
@@ -156,9 +156,10 @@ ConstantNode.prototype.toString = function() {
 
 /**
  * Get LaTeX representation
+ * @param {Object|function} callback(s)
  * @return {String} str
  */
-ConstantNode.prototype.toTex = function() {
+ConstantNode.prototype._toTex = function(callbacks) {
   var value = this.value,
       index;
   switch (this.valueType) {
diff --git a/lib/expression/node/FunctionAssignmentNode.js b/lib/expression/node/FunctionAssignmentNode.js
index 629360ca1..03fe8f43d 100644
--- a/lib/expression/node/FunctionAssignmentNode.js
+++ b/lib/expression/node/FunctionAssignmentNode.js
@@ -110,13 +110,14 @@ FunctionAssignmentNode.prototype.toString = function() {
 
 /**
  * get LaTeX representation
+ * @param {Object|function} callback(s)
  * @return {String} str
  */
-FunctionAssignmentNode.prototype.toTex = function() {
+FunctionAssignmentNode.prototype._toTex = function(callbacks) {
   var precedence = operators.getPrecedence(this);
   var exprPrecedence = operators.getPrecedence(this.expr);
 
-  var expr = this.expr.toTex();
+  var expr = this.expr.toTex(callbacks);
   if ((exprPrecedence !== null) && (exprPrecedence <= precedence)) {
     //adds visible round brackets
     expr = latex.addBraces(expr, true);
@@ -127,7 +128,7 @@ FunctionAssignmentNode.prototype.toTex = function() {
   }
 
   return latex.toFunction(this.name)
-       + latex.addBraces(this.params.map(latex.toSymbol).join(', '), true) + '='
+       + latex.addBraces(this.params.map(latex.toSymbol).join(', '), true) + '=' //FIXME, this doesn't call toTex on the parameters AFAIK (toSymbol)
        + expr;
 };
 
diff --git a/lib/expression/node/FunctionNode.js b/lib/expression/node/FunctionNode.js
index b36255502..fcce31025 100644
--- a/lib/expression/node/FunctionNode.js
+++ b/lib/expression/node/FunctionNode.js
@@ -113,10 +113,11 @@ FunctionNode.prototype.toString = function() {
 
 /**
  * Get LaTeX representation
+ * @param {Object|function} callback(s)
  * @return {String} str
  */
-FunctionNode.prototype.toTex = function() {
-  return latex.toArgs(this);
+FunctionNode.prototype._toTex = function(callbacks) {
+  return latex.toArgs(this, callbacks);
 };
 
 /**
diff --git a/lib/expression/node/IndexNode.js b/lib/expression/node/IndexNode.js
index cb8519a22..ed12171f7 100644
--- a/lib/expression/node/IndexNode.js
+++ b/lib/expression/node/IndexNode.js
@@ -209,10 +209,11 @@ IndexNode.prototype.toString = function() {
 
 /**
  * Get LaTeX representation
+ * @param {Object|function} callback(s)
  * @return {String} str
  */
-IndexNode.prototype.toTex = function() {
-  return this.object.toTex() + '[' + this.ranges.join(', ') + ']';
+IndexNode.prototype._toTex = function(callbacks) {
+  return this.object.toTex(callbacks) + '[' + this.ranges.join(', ') + ']';
 };
 
-module.exports = IndexNode;
\ No newline at end of file
+module.exports = IndexNode;
diff --git a/lib/expression/node/Node.js b/lib/expression/node/Node.js
index 5b97d5693..7d4813dd0 100644
--- a/lib/expression/node/Node.js
+++ b/lib/expression/node/Node.js
@@ -221,11 +221,62 @@ Node.prototype.toString = function() {
 };
 
 /**
- * Get LaTeX representation
+ * 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() {
-  return '';
+Node.prototype.toTex = function(callback) {
+  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 callback === 'object') {
+    if ((this.type === 'FunctionNode') && callback.hasOwnProperty(this.name)) {
+      //if callback is a map of callback functions and this is a FunctionNode
+      customTex = callback[this.name](this, callback);
+    }
+  }
+  else if (typeof callback === 'function') {
+    //if callback is a function
+    customTex = callback(this, callback);
+  }
+  else if ((typeof callback === 'string') && (this.type === 'ArrayNode')) {
+    //FIXME this is only a workaround for a breaking change,
+    //remove this in version2
+    this.latexType = callback;
+  }
+  else if (typeof callback !== 'undefined') {
+    throw new TypeError('Object or function expected as callback');
+  }
+
+  if (typeof customTex !== 'undefined') {
+    return customTex;
+  }
+
+  return this._toTex(callback);
+};
+
+/**
+ * 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');
 };
 
 /**
diff --git a/lib/expression/node/OperatorNode.js b/lib/expression/node/OperatorNode.js
index 5c97904e4..ee8db2afa 100644
--- a/lib/expression/node/OperatorNode.js
+++ b/lib/expression/node/OperatorNode.js
@@ -236,9 +236,10 @@ OperatorNode.prototype.toString = function() {
 
 /**
  * Get LaTeX representation
+ * @param {Object|function} callback(s)
  * @return {String} str
  */
-OperatorNode.prototype.toTex = function() {
+OperatorNode.prototype._toTex = function(callbacks) {
  var args = this.args; 
  var parens = calculateNecessaryParentheses(this, args);
  var op = latex.toOperator(this.op); //operator
@@ -247,7 +248,7 @@ OperatorNode.prototype.toTex = function() {
    case 1: //unary operators
      var assoc = operators.getAssociativity(this);
 
-     var operand = args[0].toTex();
+     var operand = args[0].toTex(callbacks);
      if (parens[0]) {
        operand = latex.addBraces(operand, true);
      }
@@ -265,9 +266,9 @@ OperatorNode.prototype.toTex = function() {
    case 2: //binary operators
      var lhs = args[0]; //left hand side
      //reminder: if parens[0] is false, this puts it in curly braces
-     var lhsTex = latex.addBraces(lhs.toTex(), parens[0]);
+     var lhsTex = latex.addBraces(lhs.toTex(callbacks), parens[0]);
      var rhs = args[1]; //right hand side
-     var rhsTex = latex.addBraces(rhs.toTex(), parens[1]);
+     var rhsTex = latex.addBraces(rhs.toTex(callbacks), parens[1]);
 
      switch (this.getIdentifier()) {
        case 'OperatorNode:divide':
@@ -275,7 +276,7 @@ OperatorNode.prototype.toTex = function() {
          return op + lhsTex + rhsTex;
 
        case 'OperatorNode:to':
-         rhsTex = latex.toUnit(rhs.toTex());
+         rhsTex = latex.toUnit(rhs.toTex(callbacks));
          rhsTex = latex.addBraces(rhsTex, parens[1]);
          break;
      }
diff --git a/lib/expression/node/RangeNode.js b/lib/expression/node/RangeNode.js
index f1f08b2bb..d3af794ee 100644
--- a/lib/expression/node/RangeNode.js
+++ b/lib/expression/node/RangeNode.js
@@ -121,14 +121,15 @@ RangeNode.prototype.toString = function() {
 
 /**
  * Get LaTeX representation
+ * @params {Object|function} callback(s)
  * @return {String} str
  */
-RangeNode.prototype.toTex = function() {
-  var str = this.start.toTex();
+RangeNode.prototype._toTex = function(callbacks) {
+  var str = this.start.toTex(callbacks);
   if (this.step) {
-    str += ':' + this.step.toTex();
+    str += ':' + this.step.toTex(callbacks);
   }
-  str += ':' + this.end.toTex();
+  str += ':' + this.end.toTex(callbacks);
 
   return str;
 };
diff --git a/lib/expression/node/SymbolNode.js b/lib/expression/node/SymbolNode.js
index e6053d379..6d807d31d 100644
--- a/lib/expression/node/SymbolNode.js
+++ b/lib/expression/node/SymbolNode.js
@@ -99,11 +99,12 @@ SymbolNode.prototype.toString = function() {
 
 /**
  * Get LaTeX representation
+ * @param {Object|function} callback(s)
  * @return {String} str
  * @override
  */
-SymbolNode.prototype.toTex = function() {
-  return latex.toSymbol(this.name);
+SymbolNode.prototype._toTex = function(callbacks) {
+  return latex.toSymbol(this.name, callbacks);
 };
 
 module.exports = SymbolNode;
diff --git a/lib/expression/node/UpdateNode.js b/lib/expression/node/UpdateNode.js
index 523019d9e..323ad1f12 100644
--- a/lib/expression/node/UpdateNode.js
+++ b/lib/expression/node/UpdateNode.js
@@ -84,10 +84,11 @@ UpdateNode.prototype.toString = function() {
 
 /**
  * Get LaTeX representation
+ * @param {Object|function} callback(s)
  * @return {String}
  */
-UpdateNode.prototype.toTex = function() {
-  return this.index.toTex() + ' = ' + this.expr.toTex();
+UpdateNode.prototype._toTex = function(callbacks) {
+  return this.index.toTex(callbacks) + ' = ' + this.expr.toTex(callbacks);
 };
 
 module.exports = UpdateNode;
diff --git a/lib/util/latex.js b/lib/util/latex.js
index 176479d59..ed6afd33d 100644
--- a/lib/util/latex.js
+++ b/lib/util/latex.js
@@ -286,7 +286,7 @@ exports.addBraces = function(s, brace, type) {
   return braces[0] + s + braces[1];
 };
 
-exports.toArgs = function(that) {
+exports.toArgs = function(that, customFunctions) {
   var name = that.name,
       args = that.args,
       func = exports.toSymbol(that.name),
@@ -358,13 +358,13 @@ exports.toArgs = function(that) {
           op = '!';
         }
         else {
-          return '{\\left(' + args[0].toTex() + '\\right)!}';
+          return '{\\left(' + args[0].toTex(customFunctions) + '\\right)!}';
         }
       }
       else {
         // op = 'P';
-        var n = args[0].toTex(),
-            k = args[1].toTex();
+        var n = args[0].toTex(customFunctions),
+            k = args[1].toTex(customFunctions);
         return '\\frac{' + n + '!}{\\left(' + n + ' - ' + k + '\\right)!}';
       }
       break;
@@ -385,7 +385,7 @@ exports.toArgs = function(that) {
       type = 'lr';
 
       if (args.length === 2) {
-        var tmp = args[1].toTex();
+        var tmp = args[1].toTex(customFunctions);
 
         if (tmp === '\\text{inf}') {
           tmp = '\\infty';
@@ -417,7 +417,7 @@ exports.toArgs = function(that) {
       type = 'lr';
 
       if (args.length === 2) {
-        suffix = '_' + exports.addBraces(args[1].toTex());
+        suffix = '_' + exports.addBraces(args[1].toTex(customFunctions));
         args = [args[0]];
       }
       break;
@@ -437,7 +437,7 @@ exports.toArgs = function(that) {
     case 'log':
       var base = 'e';
       if (args.length === 2) {
-        base = args[1].toTex();
+        base = args[1].toTex(customFunctions);
         func = '\\log_{' + base + '}';
         args = [args[0]];
       }
@@ -466,7 +466,11 @@ exports.toArgs = function(that) {
 
     case 'det':
       if (that.args[0] instanceof ArrayNode) {
-        return that.args[0].toTex('vmatrix');
+        //FIXME passing 'vmatrix' like that is really ugly
+        that.args[0].latexType = 'vmatrix';
+        var latex = that.args[0].toTex(customFunctions);
+        delete that.args[0].latexType;
+        return latex;
       }
 
       brace = 'vmatrix';
@@ -480,7 +484,7 @@ exports.toArgs = function(that) {
 
   if (op !== null) {
     brace = (op === '+' || op === '-');
-    texParams = (new OperatorNode(op, name, args)).toTex();
+    texParams = (new OperatorNode(op, name, args)).toTex(customFunctions);
   }
   else {
     op = ', ';
@@ -491,7 +495,7 @@ exports.toArgs = function(that) {
   }
 
   texParams = texParams || args.map(function(param) {
-    return '{' + param.toTex() + '}'  ;
+    return '{' + param.toTex(customFunctions) + '}'  ;
   }).join(op);
 
   return prefix + (showFunc ? func : '') +


Test patch:
diff --git a/test/expression/node/ArrayNode.test.js b/test/expression/node/ArrayNode.test.js
index 56259354c..fcfd7e978 100644
--- a/test/expression/node/ArrayNode.test.js
+++ b/test/expression/node/ArrayNode.test.js
@@ -236,4 +236,29 @@ describe('ArrayNode', function() {
     assert.equal(n.toTex(), '\\begin{bmatrix}1&2\\\\3&4\\\\\\end{bmatrix}');
   });
 
+  it ('should LaTeX an ArrayNode with custom toTex', function () {
+    //Also checks if the custom functions get passed on to the children
+    var customFunction = function (node, callback) {
+      if (node.type === 'ArrayNode') {
+        var latex = '\\left[';
+        node.nodes.forEach(function (node) {
+          latex += node.toTex(callback) + ', ';
+        });
+
+        latex += '\\right]';
+        return latex;
+      }
+      else if (node.type === 'ConstantNode') {
+        return 'const\\left(' + node.value + ', ' + node.valueType + '\\right)'
+      }
+    };
+
+    var a = new ConstantNode(1);
+    var b = new ConstantNode(2);
+
+    var n = new ArrayNode([a, b]);
+
+    assert.equal(n.toTex(customFunction), '\\left[const\\left(1, number\\right), const\\left(2, number\\right), \\right]');
+  });
+
 });
diff --git a/test/expression/node/AssignmentNode.test.js b/test/expression/node/AssignmentNode.test.js
index 8c555081d..86cb862dd 100644
--- a/test/expression/node/AssignmentNode.test.js
+++ b/test/expression/node/AssignmentNode.test.js
@@ -225,4 +225,22 @@ describe('AssignmentNode', function() {
     assert.equal(n.toTex(), '{b}=\\left({{a}={2}}\\right)');
   });
 
+  it ('should LaTeX an AssignmentNode with custom toTex', function () {
+    //Also checks if custom funcions get passed to the children
+    var customFunction = function (node, callback) {
+      if (node.type === 'AssignmentNode') {
+        return node.name + '\\mbox{equals}' + node.expr.toTex(callback);
+      }
+      else if (node.type === 'ConstantNode') {
+        return 'const\\left(' + node.value + ', ' + node.valueType + '\\right)'
+      }
+    };
+
+    var a = new ConstantNode(1);
+
+    var n = new AssignmentNode('a', a);
+
+    assert.equal(n.toTex(customFunction), 'a\\mbox{equals}const\\left(1, number\\right)');
+  });
+
 });
diff --git a/test/expression/node/BlockNode.test.js b/test/expression/node/BlockNode.test.js
index db8d66f6e..8514fb00c 100644
--- a/test/expression/node/BlockNode.test.js
+++ b/test/expression/node/BlockNode.test.js
@@ -256,4 +256,28 @@ describe('BlockNode', function() {
     assert.equal(n.toTex(), '5\n{foo}={3};\nfoo');
   });
 
+  it ('should LaTeX a BlockNode with custom toTex', function () {
+    //Also checks if the custom functions get passed on to the children
+    var customFunction = function (node, callback) {
+      if (node.type === 'BlockNode') {
+        var latex = '';
+        node.blocks.forEach(function (block) {
+          latex += block.node.toTex(callback) + '; ';
+        });
+
+        return latex;
+      }
+      else if (node.type === 'ConstantNode') {
+        return 'const\\left(' + node.value + ', ' + node.valueType + '\\right)'
+      }
+    };
+
+    var a = new ConstantNode(1);
+    var b = new ConstantNode(2);
+
+    var n = new BlockNode([{node: a}, {node: b}]);
+
+    assert.equal(n.toTex(customFunction), 'const\\left(1, number\\right); const\\left(2, number\\right); ');
+  });
+
 });
diff --git a/test/expression/node/ConditionalNode.test.js b/test/expression/node/ConditionalNode.test.js
index e50ab1305..5b4001cec 100644
--- a/test/expression/node/ConditionalNode.test.js
+++ b/test/expression/node/ConditionalNode.test.js
@@ -260,4 +260,26 @@ describe('ConditionalNode', function() {
     assert.equal(n.toTex(), '\\left\\{\\begin{array}{l l}{{a}={2}}, &\\quad{\\text{if}\\;true}\\\\{{b}={3}}, &\\quad{\\text{otherwise}}\\end{array}\\right.');
   });
 
+  it ('should LaTeX a ConditionalNode with custom toTex', function () {
+    //Also checks if the custom functions get passed on to the children
+    var customFunction = function (node, callback) {
+      if (node.type === 'ConditionalNode') {
+        return 'if ' + node.condition.toTex(callback)
+          + ' then ' + node.trueExpr.toTex(callback)
+          + ' else ' + node.falseExpr.toTex(callback);
+      }
+      else if (node.type === 'ConstantNode') {
+        return 'const\\left(' + node.value + ', ' + node.valueType + '\\right)'
+      }
+    };
+
+    var a = new ConstantNode(1);
+    var b = new ConstantNode(2);
+    var c = new ConstantNode(3);
+
+    var n = new ConditionalNode(a, b, c);
+
+    assert.equal(n.toTex(customFunction), 'if const\\left(1, number\\right) then const\\left(2, number\\right) else const\\left(3, number\\right)');
+  });
+
 });
diff --git a/test/expression/node/ConstantNode.test.js b/test/expression/node/ConstantNode.test.js
index a287de12b..c572377d6 100644
--- a/test/expression/node/ConstantNode.test.js
+++ b/test/expression/node/ConstantNode.test.js
@@ -139,4 +139,17 @@ describe('ConstantNode', function() {
     assert.equal(new ConstantNode('null', 'null').toTex(), 'null');
   });
 
+  it ('should LaTeX a ConstantNode with custom toTex', function () {
+    //Also checks if the custom functions get passed on to the children
+    var customFunction = function (node, callback) {
+      if (node.type === 'ConstantNode') {
+        return 'const\\left(' + node.value + ', ' + node.valueType + '\\right)'
+      }
+    };
+
+    var n = new ConstantNode(1);
+
+    assert.equal(n.toTex(customFunction), 'const\\left(1, number\\right)');
+  });
+
 });
diff --git a/test/expression/node/FunctionAssignmentNode.test.js b/test/expression/node/FunctionAssignmentNode.test.js
index 9b6209ea7..a7d8cff62 100644
--- a/test/expression/node/FunctionAssignmentNode.test.js
+++ b/test/expression/node/FunctionAssignmentNode.test.js
@@ -206,4 +206,29 @@ describe('FunctionAssignmentNode', function() {
 
     assert.equal(n.toTex(), 'f\\left({x}\\right)=\\left({{a}={2}}\\right)');
   });
+
+  it ('should LaTeX a FunctionAssignmentNode with custom toTex', function () {
+    //Also checks if the custom functions get passed on to the children
+    var customFunction = function (node, callback) {
+      if (node.type === 'FunctionAssignmentNode') {
+        var latex = '\\mbox{' + node.name + '}\\left(';
+        node.params.forEach(function (param) {
+          latex += param + ', ';
+        });
+
+        latex += '\\right)=' + node.expr.toTex(callback);
+        return latex;
+      }
+      else if (node.type === 'ConstantNode') {
+        return 'const\\left(' + node.value + ', ' + node.valueType + '\\right)'
+      }
+    };
+
+    var a = new ConstantNode(1);
+
+    var n = new FunctionAssignmentNode('func', ['x'], a);
+
+    assert.equal(n.toTex(customFunction), '\\mbox{func}\\left(x, \\right)=const\\left(1, number\\right)');
+  });
+
 });
diff --git a/test/expression/node/FunctionNode.test.js b/test/expression/node/FunctionNode.test.js
index d5e743746..df3a98882 100644
--- a/test/expression/node/FunctionNode.test.js
+++ b/test/expression/node/FunctionNode.test.js
@@ -288,4 +288,48 @@ describe('FunctionNode', function() {
     assert.equal(n.getIdentifier(), 'FunctionNode:factorial');
   });
 
+  it ('should LaTeX a FunctionNode with custom toTex', function () {
+    //Also checks if the custom functions get passed on to the children
+    var customFunction = function (node, callback) {
+      if (node.type === 'FunctionNode') {
+        var latex = '\\mbox{' + node.name + '}\\left(';
+        node.args.forEach(function (arg) {
+          latex += arg.toTex(callback) + ', ';
+        });
+        latex += '\\right)';
+        return latex;
+      }
+      else if (node.type === 'ConstantNode') {
+        return 'const\\left(' + node.value + ', ' + node.valueType + '\\right)'
+      }
+    };
+
+    var a = new ConstantNode(1);
+    var b = new ConstantNode(2);
+
+    var n1 = new FunctionNode('add', [a, b]);
+    var n2 = new FunctionNode('subtract', [a, b]);
+
+    assert.equal(n1.toTex(customFunction), '\\mbox{add}\\left(const\\left(1, number\\right), const\\left(2, number\\right), \\right)');
+    assert.equal(n2.toTex(customFunction), '\\mbox{subtract}\\left(const\\left(1, number\\right), const\\left(2, number\\right), \\right)');
+  });
+
+  it ('should LaTeX a FunctionNode with custom toTex for a single function', function () {
+    //Also checks if the custom functions get passed on to the children
+    var customFunction = {
+      'add': function (node, callbacks) {
+        return node.args[0].toTex(callbacks) 
+          + ' ' + node.name + ' ' 
+          + node.args[1].toTex(callbacks);
+      }
+    };
+
+    var a = new ConstantNode(1);
+    var b = new ConstantNode(2);
+
+    var n = new FunctionNode('add', [a, b]);
+
+    assert.equal(n.toTex(customFunction), '1 add 2');
+  });
+
 });
diff --git a/test/expression/node/IndexNode.test.js b/test/expression/node/IndexNode.test.js
index dc041b3e9..149d16bc6 100644
--- a/test/expression/node/IndexNode.test.js
+++ b/test/expression/node/IndexNode.test.js
@@ -285,4 +285,29 @@ describe('IndexNode', function() {
     assert.equal(n2.toTex(), 'a[]')
   });
 
+  it ('should LaTeX an IndexNode with custom toTex', function () {
+    //Also checks if the custom functions get passed on to the children
+    var customFunction = function (node, callback) {
+      if (node.type === 'IndexNode') {
+        var latex = node.object.toTex(callback) + ' at ';
+        node.ranges.forEach(function (range) {
+          latex += range.toTex(callback) + ', ';
+        });
+
+        return latex;
+      }
+      else if (node.type === 'ConstantNode') {
+        return 'const\\left(' + node.value + ', ' + node.valueType + '\\right)'
+      }
+    };
+
+    var a = new SymbolNode('a');
+    var b = new ConstantNode(1);
+    var c = new ConstantNode(2);
+
+    var n = new IndexNode(a, [b, c]);
+
+    assert.equal(n.toTex(customFunction), 'a at const\\left(1, number\\right), const\\left(2, number\\right), ');
+  });
+
 });
diff --git a/test/expression/node/Node.test.js b/test/expression/node/Node.test.js
index 355856d16..18868968b 100644
--- a/test/expression/node/Node.test.js
+++ b/test/expression/node/Node.test.js
@@ -74,9 +74,29 @@ describe('Node', function() {
     assert.equal(node.toString(), '');
   });
 
-  it ('should LaTeX a Node', function () {
-    var node = new Node();
-    assert.equal(node.toTex(), '');
+  it ('should throw an error when calling _toTex', function () {
+    assert.throws(function () {
+      var node = new Node();
+      node.type = 'SpecialNode';  //this is necessary because toTex
+                                  //returns '' for a Node
+      node._toTex();
+    }, /_toTex not implemented for this Node/);
+  });
+
+  it ('should ignore custom toTex if it returns nothing', function () {
+    var callback1 = function (node, callback) {};
+    var callback2 = {
+      bla: function (node, callbacks) {}
+    };
+    var mymath = math.create();
+    mymath.expression.node.Node.prototype._toTex = function () {
+      return 'default';
+    };
+    var n1 = new mymath.expression.node.Node();
+    var n2 = new mymath.expression.node.FunctionNode('bla', []);
+    
+    assert.equal(n1.toTex(callback1), 'default');
+    assert.equal(n2.toTex(callback2), 'bla\\left({}\\right)');
   });
 
   it ('should throw an error in case of wrong arguments for compile', function () {
diff --git a/test/expression/node/OperatorNode.test.js b/test/expression/node/OperatorNode.test.js
index 07ded28f0..bd58ff669 100644
--- a/test/expression/node/OperatorNode.test.js
+++ b/test/expression/node/OperatorNode.test.js
@@ -378,4 +378,48 @@ describe('OperatorNode', function() {
     assert.equal(n.getIdentifier(), 'OperatorNode:add');
   });
 
+  it ('should LaTeX an OperatorNode with custom toTex', function () {
+    //Also checks if the custom functions get passed on to the children
+    var customFunction = function (node, callback) {
+      if (node.type === 'OperatorNode') {
+        return node.op + node.fn + '(' 
+          + node.args[0].toTex(callback)
+          + ', ' +  node.args[1].toTex(callback) + ')';
+      }
+      else if (node.type === 'ConstantNode') {
+        return 'const\\left(' + node.value + ', ' + node.valueType + '\\right)'
+      }
+    };
+
+    var a = new ConstantNode(1);
+    var b = new ConstantNode(2);
+
+    var n1 = new OperatorNode('+', 'add', [a, b]);
+	var n2 = new OperatorNode('-', 'subtract', [a, b]);
+
+    assert.equal(n1.toTex(customFunction), '+add(const\\left(1, number\\right), const\\left(2, number\\right))');
+    assert.equal(n2.toTex(customFunction), '-subtract(const\\left(1, number\\right), const\\left(2, number\\right))');
+  });
+
+  it ('should LaTeX an OperatorNode with custom toTex for a single operator', function () {
+    //Also checks if the custom functions get passed on to the children
+    var customFunction = function (node, callback) {
+      if ((node.type === 'OperatorNode') && (node.fn === 'add')) {
+        return node.args[0].toTex(callback)
+          + node.op + node.fn + node.op + 
+          node.args[1].toTex(callback);
+      }
+      else if (node.type === 'ConstantNode') {
+        return 'const\\left(' + node.value + ', ' + node.valueType + '\\right)'
+      }
+    };
+
+    var a = new ConstantNode(1);
+    var b = new ConstantNode(2);
+
+    var n = new OperatorNode('+', 'add', [a, b]);
+
+    assert.equal(n.toTex(customFunction), 'const\\left(1, number\\right)+add+const\\left(2, number\\right)');
+  });
+
 });
diff --git a/test/expression/node/RangeNode.test.js b/test/expression/node/RangeNode.test.js
index a69c529cd..ae9ab51b4 100644
--- a/test/expression/node/RangeNode.test.js
+++ b/test/expression/node/RangeNode.test.js
@@ -289,4 +289,26 @@ describe('RangeNode', function() {
     assert.equal(n.toTex(), '0:2:10');
   });
 
+  it ('should LaTeX a RangeNode with custom toTex', function () {
+    //Also checks if the custom functions get passed on to the children
+    var customFunction = function (node, callback) {
+      if (node.type === 'RangeNode') {
+        return 'from ' + node.start.toTex(callback)
+          + ' to ' + node.end.toTex(callback)
+          + ' with steps of ' + node.step.toTex(callback);
+      }
+      else if (node.type === 'ConstantNode') {
+        return 'const\\left(' + node.value + ', ' + node.valueType + '\\right)'
+      }
+    };
+
+    var a = new ConstantNode(1);
+    var b = new ConstantNode(2);
+    var c = new ConstantNode(3);
+
+    var n = new RangeNode(a, b, c);
+
+    assert.equal(n.toTex(customFunction), 'from const\\left(1, number\\right) to const\\left(2, number\\right) with steps of const\\left(3, number\\right)');
+  });
+
 });
diff --git a/test/expression/node/SymbolNode.test.js b/test/expression/node/SymbolNode.test.js
index 33932fbd2..7152acf29 100644
--- a/test/expression/node/SymbolNode.test.js
+++ b/test/expression/node/SymbolNode.test.js
@@ -107,4 +107,17 @@ describe('SymbolNode', function() {
     assert.equal(s.toTex(), 'foo');
   });
 
+  it ('should LaTeX a SymbolNode with custom toTex', function () {
+    //Also checks if the custom functions get passed on to the children
+    var customFunction = function (node, callback) {
+      if (node.type === 'SymbolNode') {
+        return 'symbol(' + node.name + ')';
+      }
+    };
+
+    var n = new SymbolNode('a');
+
+    assert.equal(n.toTex(customFunction), 'symbol(a)');
+  });
+
 });
diff --git a/test/expression/node/UpdateNode.test.js b/test/expression/node/UpdateNode.test.js
index 402029f84..543ef1ae7 100644
--- a/test/expression/node/UpdateNode.test.js
+++ b/test/expression/node/UpdateNode.test.js
@@ -322,4 +322,34 @@ describe('UpdateNode', function() {
     assert.equal(n.toString(), 'a[2, 1] = 5');
   });
 
+  it ('should LaTeX an UpdateNode with custom toTex', function () {
+    //Also checks if the custom functions get passed on to the children
+    var customFunction = function (node, callback) {
+      if (node.type === 'UpdateNode') {
+        return node.index.toTex(callback) + ' equals ' + node.expr.toTex(callback);
+      }
+      else if (node.type === 'IndexNode') {
+        var latex = node.object.toTex(callback) + ' at ';
+        node.ranges.forEach(function (range) {
+          latex += range.toTex(callback) + ', ';
+        });
+        return latex;
+      }
+      else if (node.type === 'ConstantNode') {
+        return 'const\\left(' + node.value + ', ' + node.valueType + '\\right)'
+      }
+    };
+
+    var a = new SymbolNode('a');
+    var ranges = [
+      new ConstantNode(2),
+      new ConstantNode(1)
+    ];
+    var v = new ConstantNode(5);
+
+    var n = new UpdateNode(new IndexNode(a, ranges), v);
+
+    assert.equal(n.toTex(customFunction), 'a at const\\left(2, number\\right), const\\left(1, number\\right),  equals const\\left(5, number\\right)');
+  });
+
 });