Repository: josdejong/mathjs
Pull request #325 (2015-04-16 20:40:27)

Show less unnecessary parentheses in LaTeX output of OperatorNodes
See #290. This add's new flags to the precedence list in `list/expression/operators.js` to disable parentheses for certain nodes only for the LaTeX output and uses those flags to simplify fractions, conditions and exponents.


Patch:
diff --git a/lib/expression/node/OperatorNode.js b/lib/expression/node/OperatorNode.js
index 36567432e..1d24e5cf0 100644
--- a/lib/expression/node/OperatorNode.js
+++ b/lib/expression/node/OperatorNode.js
@@ -102,10 +102,11 @@ OperatorNode.prototype.clone = function() {
  *
  * @param {OperatorNode} root
  * @param {Node[]} arguments
+ * @param {bool}
  * @return {bool[]}
  * @private
  */
-function calculateNecessaryParentheses (root, args) {
+function calculateNecessaryParentheses (root, args, latex) {
   //precedence of the root OperatorNode
   var precedence = operators.getPrecedence(root);
   var associativity = operators.getAssociativity(root);
@@ -115,6 +116,19 @@ function calculateNecessaryParentheses (root, args) {
       //precedence of the operand
       var operandPrecedence = operators.getPrecedence(args[0]);
 
+      //handle special cases for LaTeX, where some of the parentheses aren't needed
+      if (latex && (operandPrecedence !== null)) {
+        var operandIdentifier = args[0].getIdentifier();
+        var rootIdentifier = root.getIdentifier();
+        if (operators.properties[precedence][rootIdentifier].latexLeftParens === false) {
+          return [false];
+        }
+
+        if (operators.properties[operandPrecedence][operandIdentifier].latexParens === false) {
+          return [false];
+        }
+      }
+
       if (operandPrecedence === null) {
         //if the operand has no defined precedence, no parens are needed
         return [false];
@@ -178,6 +192,34 @@ function calculateNecessaryParentheses (root, args) {
       else {
         rhsParens = false;
       }
+
+      //handle special cases for LaTeX, where some of the parentheses aren't needed
+      if (latex) {
+        var rootIdentifier = root.getIdentifier();
+        var lhsIdentifier = root.args[0].getIdentifier();
+        var rhsIdentifier = root.args[1].getIdentifier();
+
+        if (lhsPrecedence !== null) {
+          if (operators.properties[precedence][rootIdentifier].latexLeftParens === false) {
+            lhsParens = false;
+          }
+
+          if (operators.properties[lhsPrecedence][lhsIdentifier].latexParens === false) {
+            lhsParens = false;
+          }
+        }
+
+        if (rhsPrecedence !== null) {
+          if (operators.properties[precedence][rootIdentifier].latexRightParens === false) {
+            rhsParens = false;
+          }
+
+          if (operators.properties[rhsPrecedence][rhsIdentifier].latexParens === false) {
+            rhsParens = false;
+          }
+        }
+      }
+
       return [lhsParens, rhsParens];
     default:
       //behavior is undefined, fall back to putting everything in parens
@@ -195,7 +237,7 @@ function calculateNecessaryParentheses (root, args) {
  */
 OperatorNode.prototype.toString = function() {
   var args = this.args;
-  var parens = calculateNecessaryParentheses(this, args);
+  var parens = calculateNecessaryParentheses(this, args, false);
 
   switch (args.length) {
     case 1: //unary operators
@@ -241,7 +283,7 @@ OperatorNode.prototype.toString = function() {
  */
 OperatorNode.prototype._toTex = function(callbacks) {
  var args = this.args; 
- var parens = calculateNecessaryParentheses(this, args);
+ var parens = calculateNecessaryParentheses(this, args, true);
  var op = latex.operators[this.fn];
  op = typeof op === 'undefined' ? this.op : op; //fall back to using this.op
 
diff --git a/lib/expression/operators.js b/lib/expression/operators.js
index 1f406d735..1312189b7 100644
--- a/lib/expression/operators.js
+++ b/lib/expression/operators.js
@@ -10,13 +10,27 @@
 //
 // postfix operators are left associative, prefix operators 
 // are right associative
+//
+//It's also possible to set the following properties:
+// latexParens: if set to false, this node doesn't need to be enclosed
+//              in parentheses when using LaTeX
+// latexLeftParens: if set to false, this !OperatorNode's! 
+//                  left argument doesn't need to be enclosed
+//                  in parentheses
+// latexRightParens: the same for the right argument
 var properties = [
   { //assignment
     'AssignmentNode': {},
     'FunctionAssignmentNode': {}
   },
   { //conditional expression
-    'ConditionalNode': {}
+    'ConditionalNode': {
+      latexLeftParens: false,
+      latexRightParens: false,
+      latexParens: false
+      //conditionals don't need parentheses in LaTeX because
+      //they are 2 dimensional
+    }
   },
   { //logical or
     'OperatorNode:or': {
@@ -126,7 +140,13 @@ var properties = [
     },
     'OperatorNode:divide': {
       associativity: 'left',
-      associativeWith: []
+      associativeWith: [],
+      latexLeftParens: false,
+      latexRightParens: false,
+      latexParens: false
+      //fractions don't require parentheses because
+      //they're 2 dimensional, so parens aren't needed
+      //in LaTeX
     },
     'OperatorNode:dotMultiply': {
       associativity: 'left',
@@ -163,7 +183,11 @@ var properties = [
   { //exponentiation
     'OperatorNode:pow': {
       associativity: 'right',
-      associativeWith: []
+      associativeWith: [],
+      latexRightParens: false,
+      //the exponent doesn't need parentheses in
+      //LaTeX because it's 2 dimensional
+      //(it's on top)
     },
     'OperatorNode:dotPow': {
       associativity: 'right',


Test patch:
diff --git a/test/expression/node/FunctionAssignmentNode.test.js b/test/expression/node/FunctionAssignmentNode.test.js
index 856d31842..f56b3f2ef 100644
--- a/test/expression/node/FunctionAssignmentNode.test.js
+++ b/test/expression/node/FunctionAssignmentNode.test.js
@@ -265,7 +265,7 @@ describe('FunctionAssignmentNode', function() {
     var p = new OperatorNode('^', 'pow', [o, a]);
     var n = new FunctionAssignmentNode('f', ['x'], p);
 
-    assert.equal(n.toTex(), '\\mathrm{f}\\left(\\mathrm{x}\\right):={\\left({\\frac{{\\mathrm{x}}}{{2}}}\\right) ^ {2}}');
+    assert.equal(n.toTex(), '\\mathrm{f}\\left(\\mathrm{x}\\right):={{\\frac{{\\mathrm{x}}}{{2}}} ^ {2}}');
   });
 
   it ('should LaTeX a FunctionAssignmentNode containing an AssignmentNode', function () {
diff --git a/test/expression/node/OperatorNode.test.js b/test/expression/node/OperatorNode.test.js
index ea2dff3fd..53b995fc8 100644
--- a/test/expression/node/OperatorNode.test.js
+++ b/test/expression/node/OperatorNode.test.js
@@ -301,7 +301,7 @@ describe('OperatorNode', function() {
     assert.equal(n1.toTex(), '\\left({{2} - {3}}\\right)!');
     assert.equal(n2.toTex(), '\\left({{2} + {3}}\\right)!');
     assert.equal(n3.toTex(), '\\left({{2} \\cdot {3}}\\right)!');
-    assert.equal(n4.toTex(), '\\left({\\frac{{2}}{{3}}}\\right)!');
+    assert.equal(n4.toTex(), '{\\frac{{2}}{{3}}}!');
   });
 
   it ('should LaTeX an OperatorNode with unary minus', function () {
@@ -375,7 +375,7 @@ describe('OperatorNode', function() {
 
     var add = new OperatorNode('+', 'add', [a,a]);
     var frac = new OperatorNode('/', 'divide', [add,b]);
-    assert.equal(frac.toTex(), '\\frac{\\left({{1} + {1}}\\right)}{{2}}');
+    assert.equal(frac.toTex(), '\\frac{{{1} + {1}}}{{2}}');
   });
 
   it ('should have an identifier', function () {