(function (global) {
    function PathNode(nodeText) {
        const _this = this;
        let children = [],
            text = nodeText,
            url = null;

        this.addChild = function(pathInfo) {
            // set URL at the last node in the branch
            if (pathInfo.parts.length === 0) {
                url = pathInfo.url;
                return;
            }

            let matchedChild = _this.findChild(pathInfo.parts[0]);
            if (!matchedChild) {
                matchedChild = new PathNode(pathInfo.parts[0]);
                children.push(matchedChild);
            }

            // recursion is more readable and is good enough for expected input size
            matchedChild.addChild({
                parts: pathInfo.parts.slice(1),
                url: pathInfo.url
            });
        };

        this.findChild = function (text) {
            return children.find(function (x) {
                return x.text === text;
            });
        };

        this.toObject = function () {
            return {
                text: text,
                url: url,
                children: children.map(x => x.toObject())
            }
        }
    }

    function PathTree() {
        let root = new PathNode('.');

        this.addPath = function (pathInfo) {
            root.addChild(pathInfo);
        };

        this.toObject = function () {
            return root.toObject();
        }
    }
    
    function buildTree(files) {
        let tree = new PathTree();
        
        files.forEach(function (x) {
            tree.addPath({
                // renames have pattern `old → new` -- we take the old file name
                parts: x.file.split('→')[0].trim().split('/'),
                url: x.url
            })
        });

        return tree;
    }

    global.buildTree = buildTree;
})(window);
@@ -949,20 +949,25 @@
 eturn x.
-t
+getT
 ext
+()
  === tex
@@ -989,32 +989,940 @@
 %7D);%0A        %7D;%0A%0A
+        // Merge parent and single child.%0A        this.collapse = function () %7B%0A            while (children.length === 1) %7B%0A                let singleChild = children%5B0%5D;%0A                text = %60$%7Btext%7D/$%7BsingleChild.getText()%7D%60;%0A                url = singleChild.getUrl();%0A                children = singleChild.getChildren();%0A            %7D%0A            children.forEach(x =%3E x.collapse());%0A        %7D;%0A%0A        // Sort children ascending%0A        //%0A        // Sort only by name, don't take path length into account.%0A        this.sort = function () %7B%0A            children.sort((a, b) =%3E a %3C b ? -1 : a %3E b ? 1 : 0);%0A            children.forEach(x =%3E x.sort());%0A        %7D;%0A%0A        this.getText = function () %7B%0A            return text;%0A        %7D;%0A%0A        this.getUrl = function () %7B%0A            return url;%0A        %7D;%0A%0A        this.getChildren = function () %7B%0A            return children.slice();%0A        %7D;%0A%0A
         this.toO
@@ -2258,32 +2258,182 @@
 o);%0A        %7D;%0A%0A
+        this.collapse = function () %7B%0A            root.collapse();%0A        %7D;%0A%0A        this.sort = function () %7B%0A            root.sort();%0A        %7D;%0A%0A
         this.toO
@@ -2837,16 +2837,16 @@
      %7D)%0A
-
         
@@ -2848,16 +2848,62 @@
      %7D);
+%0A        tree.collapse();%0A        tree.sort();
 %0A%0A      

Implement collapse and sort.