public/Get-Tree.ps1

function Get-Tree {
  <#
  .SYNOPSIS
  Format a tree.

  .PARAMETER TreeDSL
  The domain specific language for building the tree.

  .PARAMETER Prefixes
  A 3-element array containing the prefixes to use for formatting the tree.

  The prefixes must all have the same length.

  .PARAMETER SpacesBetweenColumns
  The number of spaces to use to seperate columns in a node. Default to 1.
  #>


  param (
    [Parameter(Mandatory)]
    [scriptblock] $TreeDSL,
    [string[]] $Prefixes = @(
      (Get-SettingValue 'TREE_IN_PREFIX' '│ '),
      (Get-SettingValue 'TREE_BRANCH_PREFIX' '├─ '),
      (Get-SettingValue 'TREE_BRANCH_PREFIX' '└─ ')
    ),
    [ValidateRange(0, 20)]
    [int] $SpacesBetweenColumns = 1
  )

  # Recursive function for formatting a tree node
  function Format-TreeNode {
    param (
      [TreeNode] $Node,
      [String] $Indent,
      [Boolean] $Last,
      [Boolean] $Root
    )

    # Print node prefix + name
    $outputPrefix = ""
    if (!$Root) {
      $outputPrefix += $Indent
      $outputPrefix += ($Last ? $Prefixes[2] : $Prefixes[1])
    }
    "${outputPrefix}$($Node.Label)"

    # Recursively increment the indentation
    if (!$Root) {
      if ($Last) {
        $Indent += (" " * $Prefixes[0].Length)
      } else {
        $Indent += $Prefixes[0]
      }
    }

    # Render children
    Format-TreeChildren -Children $Node.Children -Indent $Indent
  }

  # Utility function for rendering the children of a tree node
  function Format-TreeChildren {
    param (
      [TreeNode[]] $Children,
      [String] $Indent,
      [Boolean] $Root
    )

    # Render all children
    for ($i = 0; $i -lt $Children.Count; $i++) {
      Format-TreeNode `
        -Node $Children[$i] `
        -Indent $Indent `
        -Last ($i -eq $Children.Count -1) `
        -Root $Root
    }
  }

  # Recursive function for returning all nodes in the tree
  function Get-TreeChildren {
    param (
      [TreeNode] $Node
    )

    # Output the node (ignore the root with no columns)
    if ($Node.Columns.Count -gt 0) {
      $Node
    }

    # Recurse over the children
    $Node.Children | ForEach-Object { Get-TreeChildren $_ }
  }

  # Create a tree root
  $root = [TreeNode]::New(@())

  # Context of the tree DSL
  $invokeContext = @{
    # Build a new node
    Node = {
      param (
        [Parameter(Mandatory)]
        [string[]] $Columns,
        [scriptblock] $NodeDSL
      )

      # Add the child to the current node
      $currentNode = $Node.AddChild($Columns)

      # If a DSL scriptblock was passed, invoke it with the new node as the context
      if ($NodeDSL) {
        $NodeDSL.InvokeWithContext($invokeContext, @(New-Variable 'Node' $currentNode))
      }
    }

    # Set alignment group
    AlignmentGroup = {
      param (
        [Parameter(Mandatory)]
        [int] $AlignmentGroup
      )
      $Node.AlignmentGroup = $AlignmentGroup
    }

    # Set children alignment group
    ChildrenAlignmentGroup = {
      param (
        [Parameter(Mandatory)]
        [int] $AlignmentGroup
      )
      $Node.ChildrenAlignmentGroup = $AlignmentGroup
    }

    # Set column alignment
    ColumnAlignment = {
      param (
        [Parameter(Mandatory)]
        [int] $ColumnIndex,
        [Parameter(Mandatory)]
        [ValidateSet('Left', 'Right', 'Centered')]
        [string] $ColumnAlignment
      )
      $Node.SetColumnAlignment($ColumnIndex, $ColumnAlignment)
    }

    # Set children column alignment
    ChildrenColumnAlignment = {
      param (
        [Parameter(Mandatory)]
        [int] $ColumnIndex,
        [Parameter(Mandatory)]
        [ValidateSet('Left', 'Right', 'Centered')]
        [string] $ColumnAlignment
      )
      $Node.SetChildrenColumnAlignment($ColumnIndex, $ColumnAlignment)
    }
  }

  # Validate the prefixes are all of the same length
  $prefixLength = Confirm-ValidPrefixes $Prefixes -PrefixesCount 3 -SameLength

  # Invoke the DSL scriptblock with the root as the context
  $TreeDSL.InvokeWithContext($invokeContext, @(New-Variable 'Node' $root))

  #
  # Handle alignment groups
  #

  # Collect tree nodes per alignment groups
  $nodesPerGroup = @{}
  Get-TreeChildren $root | ForEach-Object {
    if (-Not $nodesPerGroup.ContainsKey($_.AlignmentGroup)) {
      $nodesPerGroup[$_.AlignmentGroup] = @()
    }
    $nodesPerGroup[$_.AlignmentGroup] += $_
  }

  # Align group of nodes
  $nodesPerGroup.Values | ForEach-Object {
    # Get the group of nodes to align
    $group = $_

    # Arrange in a two-dimension array the columns length of the nodes in the group
    $groupColumnsLength = @()
    $group | ForEach-Object {
      $node = $_
      $groupColumnsLength += ,@((0..($node.Columns.Count - 1)) | ForEach-Object {
        if (0 -eq $_) {
          # Include the prefix length in the column length calculation
          $node.Columns[$_].TextLength + ($node.Depth - 1) * $prefixLength
        } else {
          $node.Columns[$_].TextLength
        }
      })
    }

    # Compute the columns length and assign it to the nodes in the group
    $groupColumnsLength = @(Get-MaxArray $groupColumnsLength)
    $group | ForEach-Object {
      $cols = $_.Columns
      for ($i = 0; $i -lt [Math]::Min($groupColumnsLength.Count, $cols.Count); $i++) {
        $cols[$i].ColumnLength = $groupColumnsLength[$i]
      }
    }

    # Correct the first column length to adjust for the prefix
    $group | ForEach-Object {
      $_.Columns[0].ColumnLength -= ($_.Depth - 1) * $prefixLength
    }
  }

  # Format the tree
  $root.FormatChildren($SpacesBetweenColumns)
  Format-TreeChildren -Children $root.Children -Indent '' -Root $true
}