Search code examples
javascriptalgorithmabstract-syntax-tree

How to transform an expression tree (AST) into DOM tree (XML)


I need to allow a user to write expressions and build XML tree from the expression. My plan is to use math.js which parses and generates an expression tree, then convert that expression tree into DOM tree, and then convert DOM tree into XML using XMLSerializer. The part in bold is tricky one.

For the expression:

const node = math.parse('sqrt(2 + x)')

math.js generates the following expression tree:

 FunctionNode    sqrt
                   |
  OperatorNode     +
                  / \
  ConstantNode   2   x   SymbolNode

Now I need to convert it to DOM. I'd like to use the traversal API exposed by the library. They have traverse method, that recursively traverse all nodes in a node tree and executes given callback for this node and each of its child nodes. The path parameter in the callback is a string containing a relative JSON Path.

My first attempt that included keeping a reference to the parent DOM node didn't work as expected:

const parent = new DocumentFragment();
let tmpParent = parent;

  const node = math.parse('3 * x + 2');
  node.traverse(function(node, path, parent) {
    switch (node.type) {
      case 'OperatorNode':
        const operator = document.createElement('div');
        tmpParent.appendChild(operator);
        tmpParent = operator;
        break;
      case 'ConstantNode':
        const constant = document.createElement('span');
        tmpParent.appendChild(constant);
        tmpParent = constant;
      ...
    }
  });

So I started looking at how HAST is transformed into JSX, and they use the following approach:

walk(estree, {
    enter: function(node) {
      stack.push();
      ...
    }
    leave: function(node) {
      ...
        stack.pop();
      }
    }
  })

So I gather I'd need to somehow use stack as well. But their pattern exposes enter and leave callback points, while with traverse from math.js I have only a single point.

Is it possible to do what I'm trying to achieve with a single point using stack or else how to do it?


Solution

  • You can extend the node object with a custom property like DOM and append children to the DOM of the parent node:

    const node = math.parse('sqrt(2 + x)')
    
    node.traverse(function(node, path, parent) {
        switch (node.type) {
          case 'FunctionNode':
            node.DOM = document.createElement('div')
            break
          case 'OperatorNode':
            node.DOM = document.createElement('p');
            break;
          case 'ConstantNode':
            node.DOM = document.createElement('span');
            break;
          case 'SymbolNode':
            node.DOM = document.createElement('b');
            break
        }
        
        if (parent)
          parent.DOM.appendChild(node.DOM)
    
    });
      
    console.log(node.DOM.outerHTML)  
    <script src="https://unpkg.com/mathjs@9.4.2/lib/browser/math.js"></script>