Search code examples
javascriptrecursiondata-structuresrecursive-datastructures

How do I remove single children in a tree?


Given the following tree:

{
  "name": "svg",
  "type": "element",
  "value": "",
  "attributes": {
    "xmlns": "http://www.w3.org/2000/svg",
    "width": "1440",
    "height": "1080"
  },
  "children": [{
    "name": "g",
    "type": "element",
    "value": "",
    "attributes": {},
    "children": [{
      "name": "g",
      "type": "element",
      "value": "",
      "attributes": {},
      "children": [{
        "name": "g",
        "type": "element",
        "value": "",
        "attributes": {
          "fill": "#627580"
        },
        "children": [{
          "name": "g",
          "type": "element",
          "value": "",
          "attributes": {
            "stroke-linejoin": "round"
          },
          "children": [{
            "name": "path",
            "type": "element",
            "value": "",
            "attributes": {
              "d": "M150 0 L75 200 L225 200 Z"
            },
            "children": []
          }, {
            "name": "path",
            "type": "element",
            "value": "",
            "attributes": {
              "d": "M150 0 L75 200 L225 200 Z"
            },
            "children": []
          }, {
            "name": "path",
            "type": "element",
            "value": "",
            "attributes": {
              "d": "M150 0 L75 200 L225 200 Z"
            },
            "children": []
          }]
        }, {
          "name": "g",
          "type": "element",
          "value": "",
          "attributes": {
            "stroke-linejoin": "round"
          },
          "children": [{
            "name": "path",
            "type": "element",
            "value": "",
            "attributes": {
              "d": "M150 0 L75 200 L225 200 Z"
            },
            "children": []
          }, {
            "name": "path",
            "type": "element",
            "value": "",
            "attributes": {
              "d": "M150 0 L75 200 L225 200 Z"
            },
            "children": []
          }, {
            "name": "path",
            "type": "element",
            "value": "",
            "attributes": {
              "d": "M150 0 L75 200 L225 200 Z"
            },
            "children": []
          }]
        }]
      }]
    }]
  }]
}

...which is a representation of this HTML:

<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1080">
  <g>
    <g>
      <g fill="#627580">
        <g stroke-linejoin="round">
          <path d="M150 0 L75 200 L225 200 Z" />
          <path d="M150 0 L75 200 L225 200 Z" />
          <path d="M150 0 L75 200 L225 200 Z" />
        </g>
        <g stroke-linejoin="round">
          <path d="M150 0 L75 200 L225 200 Z" />
          <path d="M150 0 L75 200 L225 200 Z" />
          <path d="M150 0 L75 200 L225 200 Z" />
        </g>
      </g>
    </g>
  </g>
</svg>

How do I transform this data structure recursively in JavaScript so all groups <g> that have only one child <g> are collapsed and their children are assigned to its parent?

Expected result would be this:

{
  "name": "svg",
  "type": "element",
  "value": "",
  "attributes": {
    "xmlns": "http://www.w3.org/2000/svg",
    "width": "1440",
    "height": "1080"
  },
  "children": [{
    "name": "g",
    "type": "element",
    "value": "",
    "attributes": {},
    "children": [{
      "name": "g",
      "type": "element",
      "value": "",
      "attributes": {
        "stroke-linejoin": "round"
      },
      "children": [{
        "name": "path",
        "type": "element",
        "value": "",
        "attributes": {
          "d": "M150 0 L75 200 L225 200 Z"
        },
        "children": []
      }, {
        "name": "path",
        "type": "element",
        "value": "",
        "attributes": {
          "d": "M150 0 L75 200 L225 200 Z"
        },
        "children": []
      }, {
        "name": "path",
        "type": "element",
        "value": "",
        "attributes": {
          "d": "M150 0 L75 200 L225 200 Z"
        },
        "children": []
      }]
    }, {
      "name": "g",
      "type": "element",
      "value": "",
      "attributes": {
        "stroke-linejoin": "round"
      },
      "children": [{
        "name": "path",
        "type": "element",
        "value": "",
        "attributes": {
          "d": "M150 0 L75 200 L225 200 Z"
        },
        "children": []
      }, {
        "name": "path",
        "type": "element",
        "value": "",
        "attributes": {
          "d": "M150 0 L75 200 L225 200 Z"
        },
        "children": []
      }, {
        "name": "path",
        "type": "element",
        "value": "",
        "attributes": {
          "d": "M150 0 L75 200 L225 200 Z"
        },
        "children": []
      }]
    }]
  }]
}

...which is a representation of this HTML:

<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1080">
  <g>
      <g fill="#627580">
        <g stroke-linejoin="round">
          <path d="M150 0 L75 200 L225 200 Z" />
          <path d="M150 0 L75 200 L225 200 Z" />
          <path d="M150 0 L75 200 L225 200 Z" />
        </g>
        <g stroke-linejoin="round">
          <path d="M150 0 L75 200 L225 200 Z" />
          <path d="M150 0 L75 200 L225 200 Z" />
          <path d="M150 0 L75 200 L225 200 Z" />
        </g>
      </g>
  </g>
</svg>

Ideally there should not be a <g> element remaining with a single child <g>. In other words:

<g>
  <g>
  ...
  </g>
</g> 

will be transformed into

<g>
...
</g>

This is my attempt:

const collapseGroups = node => {
  if (node.children && node.children.length > 0) {
    node.children.forEach(child => {
      collapseGroups(child)
    })
    if (node.name == 'g' && 
      node.children.length == 1 && 
      node.children[0].name == 'g') {
        node.children = node.children[0].children;
    }
  }
}

NB: My input is a SVG file, which is already automatically parsed into the above mentioned JS object.


Solution

  • You can use a recursive function on each node. If it detects it is of type "g", and only has one child, then the node absorbs the child, merging also the attributes property:

    function simplify(node) {
        return  node.name === 'g' && node.children.length === 1 
                                  && node.children[0].name === 'g'
              ? { 
                    ...simplify(node.children[0]), 
                    attributes: { // Merge attributes of the two nodes that merge into one
                        ...node.attributes,
                        ...node.children[0].attributes
                    }
                }
              : {
                    ...node, 
                    children: node.children.map(simplify)
                };
    }
    
    // Sample input:
    let root = {
      "name": "svg",
      "type": "element",
      "value": "",
      "attributes": {
        "xmlns": "http://www.w3.org/2000/svg",
        "width": "1440",
        "height": "1080"
      },
      "children": [{
        "name": "g",
        "type": "element",
        "value": "",
        "attributes": {},
        "children": [{
          "name": "g",
          "type": "element",
          "value": "",
          "attributes": {},
          "children": [{
            "name": "g",
            "type": "element",
            "value": "",
            "attributes": {
              "fill": "#627580"
            },
            "children": [{
              "name": "g",
              "type": "element",
              "value": "",
              "attributes": {
                "stroke-linejoin": "round"
              },
              "children": [{
                "name": "path",
                "type": "element",
                "value": "",
                "attributes": {
                  "d": "M150 0 L75 200 L225 200 Z"
                },
                "children": []
              }, {
                "name": "path",
                "type": "element",
                "value": "",
                "attributes": {
                  "d": "M150 0 L75 200 L225 200 Z"
                },
                "children": []
              }, {
                "name": "path",
                "type": "element",
                "value": "",
                "attributes": {
                  "d": "M150 0 L75 200 L225 200 Z"
                },
                "children": []
              }]
            }, {
              "name": "g",
              "type": "element",
              "value": "",
              "attributes": {
                "stroke-linejoin": "round"
              },
              "children": [{
                "name": "path",
                "type": "element",
                "value": "",
                "attributes": {
                  "d": "M150 0 L75 200 L225 200 Z"
                },
                "children": []
              }, {
                "name": "path",
                "type": "element",
                "value": "",
                "attributes": {
                  "d": "M150 0 L75 200 L225 200 Z"
                },
                "children": []
              }, {
                "name": "path",
                "type": "element",
                "value": "",
                "attributes": {
                  "d": "M150 0 L75 200 L225 200 Z"
                },
                "children": []
              }]
            }]
          }]
        }]
      }]
    };
    
    root = simplify(root);
    
    console.log(root);