PAParsing.ps1

class IndentedDataItem {
    [string] $Value
    [IndentedDataItem[]] $Children

    [IndentedDataItem] FindChild([string] $pattern) {
        return $this.Children | Where-Object { $_.Value -match $pattern } | Select-Object -First 1
    }

    [string] ParseChildValue([string] $prefix) {
        return $this.FindChild("^$prefix.*").Value -replace $prefix
    }

    [string] ParseValue([string] $prefix) {
        return $this.Value -replace $prefix
    }

    [string] ToString() {
        return "Value: $($this.Value) Children: [$($this.Children)]"
    }
}

function Split-IndentedData {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline=$true)]
        [string]
        $inputData
    )
    begin {
        function parseChildLines([string[]] $childLines) {
            $childLines | Where-Object { $_ -ne "" } | ForEach-Object { $_.substring(1) } | Split-IndentedData
        }

        $currentObject = $null
        $childLines = @()
    }
    process {
        switch -regex ($inputData) {
            '^(\S.*)' {
                if ($null -ne $currentObject) {
                    $currentObject.Children = parseChildLines $childLines
                    # output the object to the stream
                    $currentObject
                }
                $currentObject = [IndentedDataItem] @{
                    Value = $matches.1
                }
                $childLines = @()
                continue
            }

            default {
                # append the current line to the children
                $childLines += $_
            }
        }
    }
    end {
        if ($currentObject) {
            $currentObject.Children = parseChildLines $childLines
            # output the last object to the stream
            $currentObject
        }
    }
}

function Remove-InvalidNewlines {
    <#
    .DESCRIPTION
    Removes newlines that are not part of the output formatting, but are instead part of a value.
    .NOTES
    Invalid newlines are detected by checking for lines that begin with spaces instead of tabs. This
    is not a particularly robust mechanism, but this is the only invalid case that has been found so
    far.
    #>

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline=$true)]
        [string]
        $currentLine
    )
    begin {
        $lastLine = $null
    }
    process {
        if ($null -eq $lastLine) {
            $lastLine = $currentLine
        } elseif ($currentLine -match '^ +(.*)') {
            # output the last line with this line appended
            $lastLine + $matches.1
            $lastLine = $null
        } else {
            # output the last line
            $lastLine
            
            $lastLine = $currentLine
        }
    }
    end {
        # output the last line
        $lastLine
    }
}