text: // list of identifiers of nodes in order of their precedence
  // also contains information about left/right associativity
  // and which other operator the operator is associative with
  // Example:
  // addition is associative with addition and subtraction, because:
  // (a+b)+c=a+(b+c)
  // (a+b)-c=a+(b-c)
  //
  // 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
  export const properties = [
    { // assignment
      'AssignmentNode': {},
      'FunctionAssignmentNode': {}
    },
    { // conditional expression
      'ConditionalNode': {
        latexLeftParens: false,
        latexRightParens: false,
        latexParens: false
        // conditionals don't need parentheses in LaTeX because
        // they are 2 dimensional
      }
    },
    { // logical or
      'OperatorNode:or': {
        associativity: 'left',
        associativeWith: []
      }
  
    },
    { // logical xor
      'OperatorNode:xor': {
        associativity: 'left',
        associativeWith: []
      }
    },
    { // logical and
      'OperatorNode:and': {
        associativity: 'left',
        associativeWith: []
      }
    },
    { // bitwise or
      'OperatorNode:bitOr': {
        associativity: 'left',
        associativeWith: []
      }
    },
    { // bitwise xor
      'OperatorNode:bitXor': {
        associativity: 'left',
        associativeWith: []
      }
    },
    { // bitwise and
      'OperatorNode:bitAnd': {
        associativity: 'left',
        associativeWith: []
      }
    },
    { // relational operators
      'OperatorNode:equal': {
        associativity: 'left',
        associativeWith: []
      },
      'OperatorNode:unequal': {
        associativity: 'left',
        associativeWith: []
      },
      'OperatorNode:smaller': {
        associativity: 'left',
        associativeWith: []
      },
      'OperatorNode:larger': {
        associativity: 'left',
        associativeWith: []
      },
      'OperatorNode:smallerEq': {
        associativity: 'left',
        associativeWith: []
      },
      'OperatorNode:largerEq': {
        associativity: 'left',
        associativeWith: []
      },
      'RelationalNode': {
        associativity: 'left',
        associativeWith: []
      }
    },
    { // bitshift operators
      'OperatorNode:leftShift': {
        associativity: 'left',
        associativeWith: []
      },
      'OperatorNode:rightArithShift': {
        associativity: 'left',
        associativeWith: []
      },
      'OperatorNode:rightLogShift': {
        associativity: 'left',
        associativeWith: []
      }
    },
    { // unit conversion
      'OperatorNode:to': {
        associativity: 'left',
        associativeWith: []
      }
    },
    { // range
      'RangeNode': {}
    },
    { // addition, subtraction
      'OperatorNode:add': {
        associativity: 'left',
        associativeWith: ['OperatorNode:add', 'OperatorNode:subtract']
      },
      'OperatorNode:subtract': {
        associativity: 'left',
        associativeWith: []
      }
    },
    { // multiply, divide, modulus
      'OperatorNode:multiply': {
        associativity: 'left',
        associativeWith: [
          'OperatorNode:multiply',
          'OperatorNode:divide',
          'Operator:dotMultiply',
          'Operator:dotDivide'
        ]
      },
      'OperatorNode:divide': {
        associativity: 'left',
        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',
        associativeWith: [
          'OperatorNode:multiply',
          'OperatorNode:divide',
          'OperatorNode:dotMultiply',
          'OperatorNode:doDivide'
        ]
      },
      'OperatorNode:dotDivide': {
        associativity: 'left',
        associativeWith: []
      },
      'OperatorNode:mod': {
        associativity: 'left',
        associativeWith: []
      }
    },
    { // unary prefix operators
      'OperatorNode:unaryPlus': {
        associativity: 'right'
      },
      'OperatorNode:unaryMinus': {
        associativity: 'right'
      },
      'OperatorNode:bitNot': {
        associativity: 'right'
      },
      'OperatorNode:not': {
        associativity: 'right'
      }
    },
    { // exponentiation
      'OperatorNode:pow': {
        associativity: 'right',
        associativeWith: [],
        latexRightParens: false
        // the exponent doesn't need parentheses in
        // LaTeX because it's 2 dimensional
        // (it's on top)
      },
      'OperatorNode:dotPow': {
        associativity: 'right',
        associativeWith: []
      }
    },
    { // factorial
      'OperatorNode:factorial': {
        associativity: 'left'
      }
    },
    { // matrix transpose
      'OperatorNode:transpose': {
        associativity: 'left'
      }
    }
  ]
  
  /**
   * Get the precedence of a Node.
   * Higher number for higher precedence, starting with 0.
   * Returns null if the precedence is undefined.
   *
   * @param {Node}
   * @param {string} parenthesis
   * @return {number|null}
   */
  export function getPrecedence (_node, parenthesis) {
    let node = _node
    if (parenthesis !== 'keep') {
      // ParenthesisNodes are only ignored when not in 'keep' mode
      node = _node.getContent()
    }
    const identifier = node.getIdentifier()
    for (let i = 0; i < properties.length; i++) {
      if (identifier in properties[i]) {
        return i
      }
    }
    return null
  }
  
  /**
   * Get the associativity of an operator (left or right).
   * Returns a string containing 'left' or 'right' or null if
   * the associativity is not defined.
   *
   * @param {Node}
   * @param {string} parenthesis
   * @return {string|null}
   * @throws {Error}
   */
  export function getAssociativity (_node, parenthesis) {
    let node = _node
    if (parenthesis !== 'keep') {
      // ParenthesisNodes are only ignored when not in 'keep' mode
      node = _node.getContent()
    }
    const identifier = node.getIdentifier()
    const index = getPrecedence(node, parenthesis)
    if (index === null) {
      // node isn't in the list
      return null
    }
    const property = properties[index][identifier]
  
    if (property.hasOwnProperty('associativity')) {
      if (property.associativity === 'left') {
        return 'left'
      }
      if (property.associativity === 'right') {
        return 'right'
      }
      // associativity is invalid
      throw Error('\'' + identifier + '\' has the invalid associativity \'' +
                  property.associativity + '\'.')
    }
  
    // associativity is undefined
    return null
  }
  
  /**
   * Check if an operator is associative with another operator.
   * Returns either true or false or null if not defined.
   *
   * @param {Node} nodeA
   * @param {Node} nodeB
   * @param {string} parenthesis
   * @return {bool|null}
   */
  export function isAssociativeWith (nodeA, nodeB, parenthesis) {
    // ParenthesisNodes are only ignored when not in 'keep' mode
    const a = (parenthesis !== 'keep') ? nodeA.getContent() : nodeA
    const b = (parenthesis !== 'keep') ? nodeA.getContent() : nodeB
    const identifierA = a.getIdentifier()
    const identifierB = b.getIdentifier()
    const index = getPrecedence(a, parenthesis)
    if (index === null) {
      // node isn't in the list
      return null
    }
    const property = properties[index][identifierA]
  
    if (property.hasOwnProperty('associativeWith') &&
        (property.associativeWith instanceof Array)) {
      for (let i = 0; i < property.associativeWith.length; i++) {
        if (property.associativeWith[i] === identifierB) {
          return true
        }
      }
      return false
    }
  
    // associativeWith is not defined
    return null
  }
  