Commands/Format.PS1XML/Out-FormatData.ps.ps1

function Out-FormatData {
    <#
    .Synopsis
        Takes a series of format views and format actions and outputs a format data XML
    .Description
        A Detailed Description of what the command does
    .Example
        # Create a quick view for any XML element.
        # Piping it into Out-FormatData will make one or more format views into a full format XML file
        # Piping the output of that into Add-FormatData will create a temporary module to hold the formatting data
        # There's also a Remove-FormatData and
        Write-FormatView -TypeName "System.Xml.XmlNode" -Wrap -Property "Xml" -VirtualProperty @{
            "Xml" = {
                $strWrite = New-Object IO.StringWriter
                ([xml]$_.Outerxml).Save($strWrite)
                "$strWrite"
            }
        } |
            Out-FormatData
    #>

    [OutputType([string])]
    param(
    # The Format XML Document. The XML document can be supplied directly,
    # but it's easier to use Write-FormatView to create it
    [Parameter(Mandatory=$true,
        ValueFromPipeline=$true)]
    [ValidateScript({
        if ((-not $_.View) -and (-not $_.Control) -and (-not $_.SelectionSet)) {
            throw "The root of a format XML most contain either a View or a Control element"
        }
        return $true
    })]
    [Xml]
    $FormatXml,
    # The name of the module the format.ps1xml applies to.
    # This is required if you are using colors.
    # This is required if you use any dynamic parts (named script blocks stored a /Parts) directory.
    [string]
    $ModuleName = 'EZOut',
    
    
    # The output path.
    # This can be a string or a dictionary.
    # If it is a dictionary, the keys must a be a `[string]` or `[regex]` defining a pattern, and the value will be the path.
    [ValidateTypes(
        TypeName={
            [string],[Collections.IDictionary]
        }
    )]    
    [PSObject]
    $OutputPath
    )
    begin {
       
$Aspect = {
                                                param($ast)
                                
                                                if ($ast -is [Management.Automation.Language.CommandAst]) {
                                                    $ast
                                                }
                                            }
 $views = ""
        $controls = ""
        $selectionSets = ""
        
        function findUsedParts {
                    param(
                        [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
                        [Alias('InnerText','ScriptBlock','ScriptContents')]
                        [string]$InScript,
                        
                        [PSModuleInfo[]]
                        $FromModule = @(Get-Module),
                        
                        # If set, will look for a part globally if it does not find in any of the modules.
                        [switch]
                        $AllowGlobal
                        )
                        
                        begin {
                            if (-not $script:LookedUpCommands) {
                                $script:LookedUpCommands = @{}
                            }
                            if (-not $script:CommandModuleLookup) {
                                $script:CommandModuleLookup = @{}
                            }
                            $GetVariableValue = {
                                param($name)
                                $ExecutionContext.SessionState.PSVariable.Get($name).Value
                            }            
                        }
                        
                            process {
                                $in = $_
                        
                                $inScriptBlock = try { [scriptblock]::Create($InScript) } catch { $null }
                        
                                if ($inScriptBlock.Ast) {
                                    $cmdRefs = @($inScriptBlock.Ast.FindAll($Aspect, $true))
                        
                                    foreach ($cmd in $cmdRefs) {
                                        $variableName = 
                                            if ($cmd.CommandElements[0].VariablePath) {
                                                "$($cmd.CommandElements[0].VariablePath)"
                                            } else { '' }
                                        $commandName =
                                            if ($cmd.CommandElements[0].Value) {
                                                $cmd.CommandElements[0].Value
                                            }
                        
                                        if (-not ($variableName -or $commandName)) { continue }
                                        $foundCommand = 
                                            foreach ($module in $FromModule) {
                                                $foundIt = 
                                                    if ($variableName) {
                                                        & $module $GetVariableValue $variableName
                                                    } elseif ($commandName) {
                                                        $script:CommandModuleLookup["$commandName"] = $module
                                                        $module.ExportedCommands[$commandName]
                                                    }
                                                if ($foundIt -and $variableName) {
                                                    $script:CommandModuleLookup[$variableName] = $module
                                                    if ($foundIt -is [ScriptBlock]) {
                                                        $PSBoundParameters.InScript = "$foundIt"
                                                        if ("$foundIt") {
                                                            & $MyInvocation.MyCommand.ScriptBlock @PSBoundParameters
                                                        }
                                                    }
                                                    $foundIt; break
                                                } elseif ($foundIt -and $commandName) {
                                                    $script:CommandModuleLookup[$commandName] = $module
                                                    $foundIt
                                                }
                                            }
                        
                                        if (-not $foundCommand -and $AllowGlobal) {
                                            $foundCommand = & $getVariableValue $variableName
                                        }
                                        if ($variableName) {
                                            $script:LookedUpCommands["$variableName"] = $foundCommand
                                            $PartName = "$variableName"
                                        } elseif ($commandName) {
                                            $script:LookedUpCommands["& $commandName"]  = 
                                                if ($foundCommand.ScriptBlock) {
                                                    $foundCommand.ScriptBlock
                                                } 
                                                elseif ($foundCommand.ResolvedCommand.ScriptBlock) {
                                                    $foundCommand.ResolvedCommand.ScriptBlock
                                                }
                                                else {
                                                    $isOk = $false
                                                }
                                                $PartName = "& $commandName"
                        
                                            $isOk =
                                                foreach ($attr in $foundCommand.ScriptBlock.Attributes) {
                                                    if ($attr -is [Management.Automation.CmdletAttribute]){
                                                        $extensionCommandName = (
                                                            ($attr.VerbName -replace '\s') + '-' + ($attr.NounName -replace '\s')
                                                        ) -replace '^\-' -replace '\-$'
                                                        if ('Format-Object' -match $extensionCommandName) {
                                                            $true
                                                            break
                                                        }
                                                    }
                                                }
                        
                                            if (-not $isOk) { continue }
                                        }
                        
                                        
                                        if ($script:LookedUpCommands[$partName] -and $script:LookedUpCommands[$partName] -isnot [ScriptBlock]) {
                                            continue 
                                        }
                                        [PSCustomObject][Ordered]@{
                                            Name = $PartName
                                            CommandName = $commandName
                                            VariableName = $variableName
                                            ScriptBlock = $script:LookedUpCommands[$PartName]
                                            Module = $script:CommandModuleLookup[$PartName]                    
                                            FindInput = $in
                                        }                
                                    }
                                }                                        
                        }
                
        }
        filter ReplaceParts {
                    if ($DebugPreference -ne 'silentlyContinue') {
                        $in = $_
                        if ($in.InnerText) { return $in.InnerText}
                        else { return $in }
                    }
                    $inScriptBlock = try { [scriptblock]::Create($_) } catch { $null }
                    $inScriptString = "$inScriptBlock"
                    $cmdRefs = @($inScriptBlock.Ast.FindAll($Aspect, $true))
                    $replacements = @()
                    foreach ($cmd in $cmdRefs) {
                        $partName = 
                            if ($cmd.CommandElements[0].VariablePath) {
                                "$($cmd.CommandElements[0].VariablePath)"
                            } elseif ($cmd.CommandElements[0].Value) {
                                "& $($cmd.CommandElements[0].Value)"
                            } else  { ''}
                        
                        foreach ($part in $foundParts) {
                            if ("$($part.Name)" -eq $partName) {
                                $replacements += @{
                                    Ast = $cmd.CommandElements[0]
                                    ReplacementText = if ($newPartNames.$partName) { $newPartNames.$partName} else {$partName}
                                }
                                break
                            }
                        }
                    }
                    $stringBuilder = [Text.StringBuilder]::new()
                    $stringIndex   =0            
                    $null = for ($rc = 0; $rc -lt $replacements.Length; $rc++) {
                        if ($replacements[$rc].Ast.Extent.StartOffset -gt $stringIndex) {
                            $stringBuilder.Append($inScriptString.Substring($stringIndex, $replacements[$rc].Ast.Extent.StartOffset - $stringIndex))
                        }
                        $stringBuilder.Append($replacements[$rc].ReplacementText)
                        $stringIndex = $replacements[$rc].Ast.extent.Endoffset
                    }
                    $null = $stringBuilder.Append($inScriptString.Substring($stringIndex))
                    "$stringBuilder"
                
        }
        $importFormatParts = {
            do {
                $lm = Get-Module -Name $moduleName -ErrorAction Ignore
                if (-not $lm) { continue } 
                if ($lm.FormatPartsLoaded) { break }
                $wholeScript = @(foreach ($formatFilePath in $lm.exportedFormatFiles) {         
                    foreach ($partNodeName in Select-Xml -LiteralPath $formatFilePath -XPath "/Configuration/Controls/Control/Name[starts-with(., '$')]") {
                        $ParentNode = $partNodeName.Node.ParentNode
                        "$($ParentNode.Name)={
            $($ParentNode.CustomControl.CustomEntries.CustomEntry.CustomItem.ExpressionBinding.ScriptBlock)}"

                    }
                }) -join [Environment]::NewLine
                New-Module -Name "${ModuleName}.format.ps1xml" -ScriptBlock ([ScriptBlock]::Create(($wholeScript + ';Export-ModuleMember -Variable *'))) |
                    Import-Module -Global
                $onRemove = [ScriptBlock]::Create("Remove-Module '${ModuleName}.format.ps1xml'")
                
                if (-not $lm.OnRemove) {
                    $lm.OnRemove = $onRemove
                } else {
                    $lm.OnRemove = [ScriptBlock]::Create($onRemove.ToString() + ''  + [Environment]::NewLine + $lm.OnRemove)
                }
                $lm | Add-Member NoteProperty FormatPartsLoaded $true -Force
            
            } while ($false)
            
        }
    }
    process {
        if ($FormatXml.View) {
            $views += "<View>$($FormatXml.View.InnerXml)</View>"
        } elseif ($FormatXml.Control) {
            $controls += "<Control>$($FormatXml.Control.InnerXml)</Control>"
        } elseif ($FormatXml.SelectionSet) {
            $selectionSets += "<SelectionSet>$($FormatXml.SelectionSet.InnerXml)</SelectionSet>"
        }
    }
    end {
        $newPartNames = @{}
        $configuration = "
        <!-- Generated with EZOut $($MyInvocation.MyCommand.Module.Version): Install-Module EZOut or https://github.com/StartAutomating/EZOut -->
        <Configuration>
        "

        if ($selectionSets) {
            $configuration += "<SelectionSets>$selectionSets</SelectionSets>"
        }
        if ($Controls) {
            $Configuration+="<Controls>$Controls</Controls>"
        }
        if ($Views) {
            $Configuration+="<ViewDefinitions>$Views</ViewDefinitions>"
        }
        $configuration += "</Configuration>"
        $configurationXml = [xml]$configuration
        if (-not $configurationXml) { return }
        
        # Now we need to go looking parts used within <ScriptBlock> elements.
        # Before we do, we need to determine where to look.
        if (-not $PSBoundParameters.ContainsKey('ModuleName')) # If no -ModuleName was provided,
        {
            $callStackPeek = @(Get-PSCallStack) # use call stack peeking
            $callingFile = $callStackPeek[1].InvocationInfo.MyCommand.ScriptBlock.File # to find the calling file
            $fromEzOutFile =  $callingFile -like '*.ez*.ps1' # and see if it's an EZOut file
            if ($fromEzOutFile)
            {   # If it is,
                $moduleName = ($callingFile | Split-Path -Leaf) -replace '\.ezformat\.ps1','' # guess
                Write-Warning "No -ModuleName provided, guessing $ModuleName"  # then warn that we guessed.
            }
        }
        $modulesThatMayHaveParts =
            @(
            $theModule = $null
            $theModuleExtensions = @()
            $myModule = $MyInvocation.MyCommand.ScriptBlock.Module
            $myModuleExtensions = @()
            $loadedModules = Get-Module
            foreach ($lm in $loadedModules) {
                if ($moduleName -and $lm.Name -eq $moduleName) {
                    $theModule = $lm
                    foreach ($_ in $theModule.RequiredModules) {
                        if ($moduleName -and (
                            ($_.Name -eq $moduleName) -or
                            ($_.PrivateData.PSData.Tags -contains $ModuleName))
                        ) {
                            $theModuleExtensions += $lm
                        }
                    }
                }
                foreach ($_ in $lm.RequiredModules) {
                    if ($myModule -and (
                        ($_.Name -eq $myModule.Name) -or
                        ($_.PrivateData.PSData.Tags -contains $myModule.Name))
                    ) {
                        $myModuleExtensions += $lm
                    }
                }
            }
            $theModule
            $theModuleExtensions
            $myModule
            $myModuleExtensions
            ) | Select-Object -Unique
        $foundParts = # See if the XML refers to any parts
            @($configurationXml.SelectNodes("//ScriptBlock") | Where-Object InnerText) |
                    findUsedParts -FromModule $modulesThatMayHaveParts
        # If any parts are found, we'll need to embed them and bootstrap the loader
        if ($foundParts -and $DebugPreference -eq 'silentlyContinue') { 
            if (-not $moduleName) # To do this, we need a -ModuleName, so we if we still don't have one.
            {
                Write-Error "A -ModuleName must be provided to use advanced features" # error
                return # and return.
            }
            $alreadyEmbedded = @()
            $embedControls =
                @(foreach ($part in $foundParts) { # and embed each part in a comment
                    if ($alreadyEmbedded -contains $part.Name) { continue }
                    $partName =
                        if ($part.Name -match '\w+' -or $moduleName -match '\w+') {
                            "`${${ModuleName}_$($part.Name -replace '^[&\.] ')}"
                        } else {
                            "`$${ModuleName}_$($part.Name -replace '^[&\.] ')"
                        }
                    if ($partName -and $part.ScriptBlock -and $alreadyEmbedded -notcontains $partName) {
                        $newPartNames["$($part.Name)"]= if ($part.CommandName) { "& $partName" } else { "$partName"}
                        if (-not ($part.ScriptBlock -as [ScriptBLock[]])) {
                            continue
                        }                        
                        Write-FormatView -AsControl -Name "$partName" -Action $part.ScriptBlock -TypeName 'n/a'
                    }
                    $alreadyEmbedded += $part.Name
                })
            $controlsElement =
                if (-not $configurationXml.Configuration.Controls) {
                    $configurationXml.CreateNode([Xml.XmlNodeType]::Element,'Controls','')
                } else {
                    $configurationXml.Configuration.Controls
                }
            foreach ($ec in $embedControls) {
                $ecx = [xml]$ec
                $controlsElement.InnerXml += $ecx.Control.OuterXml
            }
            if (-not $configurationXml.Configuration.Controls) { # If we didn't already have controls
                $null = $configurationXml.Configuration.AppendChild($controlsElement) # add the <Controls> element
            } else {
                $foundParts = # Otherwise, we need to find our parts again, because the XML has changed
                    @($configurationXml.SelectNodes("//ScriptBlock")) | # and we want to rewrite the part references.
                        findUsedParts -FromModule $modulesThatMayHaveParts
            }
            $lastEntryNode = $null
            $replacedItIn = @()
            foreach ($fp in $foundParts) {
                if (-not $fp.ScriptBlock) {
                    continue
                }
                $newScriptText = @(
                    if ($lastEntryNode -ne $fp.FindInput.ParentNode.ParentNode.ParentNode) {
                        # If the grandparent node is a distinct <Entry>,
                        # we need to bootload the parts (because this is a potential entry point)
                        "`$moduleName = '$($ModuleName.Replace("'","''"))'"
                        "$ImportFormatParts"
                    }
                    if ($replacedItIn -notcontains $fp.FindInput) {
                        $fp.FindInput.InnerText | ReplaceParts
                        $replacedItIn += $fp.FindInput
                    } else {
                        $fp.FindInput.InnerText
                    }
                ) -join [Environment]::NewLine
                $lastEntryNode = $fp.FindInput.ParentNode.ParentNode.ParentNode
                $fp.FindInput.InnerText = $newScriptText
            }
        }
        if (-not $configurationXml) { return }
        
        if ($OutputPath) {
            $alreadyExportedTypeNames = @{}
            $allTypeNames = @()

            if ($outputPath -is [string]) {
                $createdOutputFile = New-Item -ItemType File -Path $OutputPath -Force
                if (-not $createdOutputFile) { return }
                $configurationXml.Save($createdOutputFile.FullName)
                Get-Item -LiteralPath $createdOutputFile.FullName                
            }
            else {                
                $fileOutputs = [Ordered]@{}  
                $viewsXml = "<Views>$views</Views>" -as [xml]
                if (-not $viewsXml) { return }
                :nextView foreach ($view in $viewsXml.Views.View) {                    
                    $viewTypeNames = @($view.ViewSelectedBy.TypeName)
                    if (($OutputPath -isnot [Collections.IDictionary])) { continue } 
                    foreach ($outPath in $OutputPath.GetEnumerator()) {
                        continue nextView if ($alreadyExportedTypeNames[$viewTypeNames]) {}
                        continue if ($outPath.Key -isnot [regex] -and $outPath.Key -isnot [string])
                        continue if ($outPath.Key -is [string] -and -not ($viewTypeNames -like $outPath.Key))
                        continue if ($outPath.Key -is [Regex] -and -not ($viewTypeNames -match $outPath.Key))
                                                
                        if (-not $fileOutputs[$outPath.Value]) {
                            $fileOutputs[$outPath.Value] = @()
                        }
                        $fileOutputs[$outPath.Value] += $view
                        $alreadyExportedTypeNames[$viewTypeNames] = $kv.Value
                        continue nextView
                    }                                   
                }
                foreach ($fileOut in $fileOutputs.GetEnumerator()) {
                    $controlsInThisFile = [Ordered]@{}
                    
                    $fileViews = "
                    <ViewDefinitions>$(foreach ($view in $fileOut.Value) {
                        $customControlReferences = $view.SelectNodes(".//CustomControlName")
                        if (-not $customControlReferences) { continue }
                        $controlsXml = "<Controls>$Controls</Controls>" -as [xml]
                        foreach ($controlRef in $customControlReferences) {
                            $refName = $controlRef.InnerText.Trim()
                            if (-not $controlsInThisFile[$refName]) {
                                $controlsInThisFile[$refName] = $controlsXml.SelectSingleNode("./Controls/Control[Name='$refname']").OuterXml
                            }
                        }
                        $view.OuterXml
                    })</ViewDefinitions>
                    "

                    $fileControls = if ($controlsInThisFile.Count) {
                        "<Controls>$(@($controlsInThisFile.Values))</Controls>"
                    } else { $null }
                    $fileXml = "
                    <!-- Generated with EZOut $($MyInvocation.MyCommand.Module.Version): Install-Module EZOut or https://github.com/StartAutomating/EZOut -->
                    <Configuration>${FileViews}${FileControls}</Configuration>
                    "
 -as [xml]
                    if (-not $fileXml) { continue }
                    $filePath = $fileOut.Key
                    $createdOutputFile = New-Item -ItemType File -Path $filePath -Force
                    if (-not $createdOutputFile) { continue }
                    $fileXml.Save($createdOutputFile.FullName)
                    Get-Item -LiteralPath $createdOutputFile.FullName
                }                
            }            
        } else {
            $strWrite = [IO.StringWriter]::new()
            $configurationXml.Save($strWrite)
            return "$strWrite"
        }
        
    }
}