allcommands.ps1

### DO NOT EDIT THIS FILE DIRECTLY ###

#.ExternalHelp HelpOut-Help.xml
function Get-MAML
{
    <#
    .Synopsis
        Gets MAML help
    .Description
        Gets help for a given command, as MAML (Microsoft Assistance Markup Language) xml.
    .Example
        Get-MAML -Name Get-MAML
    .Example
        Get-Command Get-MAML | Get-MAML
    .Example
        Get-MAML -Name Get-MAML -Compact
    .Example
        Get-MAML -Name Get-MAML -XML
    .Link
        Get-Help
    .Link
        Save-MAML
    .INPUTS
        [Management.Automation.CommandInfo]
        Accepts a command
    .Outputs
        [String]
        The MAML, as a String. This is the default.
    .Outputs
        [Xml]
        The MAML, as an XmlDocument (when -XML is passed in)
    #>

    [CmdletBinding(DefaultParameterSetName='CommandInfo')]
    [OutputType([string],[xml])]
    [Alias('ConvertTo-MAML')]
    param( 
    # The name of or more commands.
    [Parameter(ParameterSetName='ByName',Position=0,ValueFromPipelineByPropertyName=$true)]
    [string[]]
    $Name,

    # The name of one or more modules.
    [Parameter(ParameterSetName='ByModule',ValueFromPipelineByPropertyName=$true)]
    [string[]]
    $Module,

    # The CommandInfo object (returned from Get-Command).
    [Parameter(Mandatory=$true,ParameterSetName='FromCommandInfo', ValueFromPipeline=$true)]
    [Management.Automation.CommandInfo[]]
    $CommandInfo,

    # If set, the generated MAML will be compact (no extra whitespace or indentation). If not set, the MAML will be indented.
    [switch]
    $Compact,
    
    # If set, will return the MAML as an XmlDocument. The default is to return the MAML as a string.
    [switch]
    $XML,
    
    # If set, the generate MAML will not contain a version number.
    # This slightly reduces the size of the MAML file, and reduces the rate of changes in the MAML file.
    [Alias('Unversioned')]
    [switch]
    $NoVersion)
    
    begin {
        # First, we need to create a list of all commands we encounter (so we can process them at the end)
        $allCommands = [Collections.ArrayList]::new()
        # Then, we want to get the type accelerators (so we don't have to keep getting them each time we're interested)
        $typeAccelerators = [PSOBject].Assembly.GetType('System.Management.Automation.TypeAccelerators')::Get

        # Next up, we're going to declare a bunch of ScriptBlocks, which we'll call to construct the XML in pieces.
        # This way we can create a nested structure (in this case, XML), by calling the pieces we want and letting them return the XML in chunks


        #region Get TypeName
        $GetTypeName = {param([Type]$t)
            # We'll want to check to see if there are any accelerators.
            if (-not $typeAccelerators -and $typeAccelerators.GetEnumerator) {  # If there weren't
                return $t.Fullname # return the fullname.
            }
             
            foreach ($_ in $typeAccelerators.GetEnumerator()) { # Loop through the accelerators.
                if ($_.Value -eq $t) { # If it's an accelrator for the target type
                    return $_.Key.Substring(0,1).ToUpper() + $_.Key.Substring(1) # return the key (and fix it's casing)
                }
            }
            return $t.Fullname # If we didn't find it in the accelerators list, return the fullname.
        }
        #endregion Get TypeName

        #region Write Type

        # Both Inputs and Outputs have the same internal tag structure for a value, so one script block handles both cases.
        $WriteType = {param($t) 
            $typename = $t.type[0].name
            $descriptionLines = $null
            
            
            if ($in.description) { # If we have a description,
                $descriptionLines = $in.Description[0].text -split "`n|`r`n" -ne '' # we we're good.
            } else { # If we didn't, it's probably because comment based help mangles things a bit (it puts everything in a long typename).
                # Let's fix this by assigning the inType from the first line, and setting the rest as description lines
                $typename, $descriptionLines = $t.type[0].Name -split "`n|`r`n" -ne ''
            }
            $typename = [Security.SecurityElement]::Escape("$typename".Trim())
            

            "<dev:type><maml:name>$typename</maml:name><maml:uri/><maml:description /></dev:type>" # Write the type information
            if ($descriptionLines) { # If we had a description
                '<maml:description>' 
                foreach ($line in $descriptionLines) { # Write each line in it's own para tag so that it renders right.
                    $esc = [Security.SecurityElement]::Escape($line)
                    "<maml:para>$esc</maml:para>"
                }
                '</maml:description>'
            }
        }
        #endregion Write Type

        #region Write Command Details
        $writeCommandDetails = {
            # The command.details tag has 5 parts we want to provide
            # * Name,
            # * Noun
            # * Verb
            # * Synopsis
            # * Version
            $Version = "<dev:version>$(if ($cmdInfo.Version) { $cmdInfo.Version.ToString() })</dev:version>"
           
            "<command:details>
                <command:name>$([Security.SecurityElement]::Escape($cmdInfo.Name))</command:name>
                <command:noun>$noun</command:noun>
                <command:verb>$verb</command:verb>
                <maml:description>
                    <maml:para>$([Security.SecurityElement]::Escape($commandHelp.Synopsis))</maml:para>
                </maml:description>
                $(if (-not $NoVersion) { $Version})
            </command:details>
            <maml:description>
                $(
                foreach ($line in @($commandHelp.Description)[0].text -split "`n|`r`n") {
                    if (-not $line) { continue }
                    "<maml:para>$([Security.SecurityElement]::Escape($Line))</maml:para>"
                    }
                )
            </maml:description>
            "

        }
        #endregion Write Command Details
        
        #region Write Parameter
        $WriteParameter = {
            # Prepare the command.parameter attributes:
            $position  = if ($param.Position -ge 0) { $param.Position } else {"named" } #* Position
            $fromPipeline = #*FromPipeline
                if ($param.ValueFromPipeline) { "True (ByValue)" }
                elseif ($param.ValueFromPipelineByPropertyName) { "True (ByPropertyName)" }
                else { "False" } 
            $isRequired = if ($param.IsMandatory) { "true" } else { "false" } #*Required
            
            
            # Pick out the help for a given parameter
            $paramHelp = foreach ($_ in $commandHelp.parameters.parameter) {
                    if ( $_.Name -eq $param.Name ){
                        $_
                        break
                    }
                }
            $paramTypeName = & $GetTypeName $param.ParameterType # and get the type name of the parameter type.
                                
            "<command:parameter required='$isRequired' position='$position' pipelineInput='$fromPipeline' aliases='' variableLength='true' globbing='false'>" #* Echo the start tag
            "<maml:name>$($param.Name)</maml:name>" #* The maml.name tag
            '<maml:description>' #*The description tag
            foreach ($d in $paramHelp.Description) { 
                "<maml:para>$([Security.SecurityElement]::Escape($d.Text))</maml:para>"
            }
            '</maml:description>' 
            #*The parameterValue tag (which oddly enough, describes the parameter type)
            "<command:parameterValue required='$isRequired' variableLength='true'>$paramTypeName</command:parameterValue>" 
            #*The type tag (which is also it's type)
            "<dev:type><maml:name>$paramTypeName</maml:name><maml:uri /></dev:type>"
            #*and an empty default value.
            '<dev:defaultValue></dev:defaultValue>'
            #* Then close the parameter tag.
            '</command:parameter>'
        }
        #endregion Write Parameter

        #region Write Parameters
        $WriteCommandParameters = {
            '<command:parameters>' # *Open the parameters tag;
            foreach ($param in ($cmdMd.Parameters.Values | Sort-Object Name)) { #*Loop through the command's parameters alphabetically
                & $WriteParameter #*Write each parameter.
            }
            '</command:parameters>' #*Close the parameters tag
        } 
        #endregion Write Parameters


        #region Write Examples
        $WriteExamples = {
            # If there were no examples, return.
            if (-not $commandHelp.Examples.example) { return }

            
            "<command:examples>" 
            foreach ($ex in $commandHelp.Examples.Example) { # For each example:
                '<command:example>' #*Start an example tag
                '<maml:title>'
                $ex.Title  #*Put it's title in a maml:title tag
                '</maml:title>'
                '<maml:introduction>'#* Put it's introduction in a maml:introduction tag
                foreach ($i in $ex.Introduction) {
                    '<maml:para>'
                    [Security.SecurityElement]::Escape($i.Text)
                    '</maml:para>'
                }
                '</maml:introduction>'
                '<dev:code>' #* Put it's code in a dev:code tag
                [Security.SecurityElement]::Escape($ex.Code)
                '</dev:code>'
                '<dev:remarks>' #* Put it's remarks in a dev:remarks tag
                foreach ($i in $ex.Remarks) {
                    if (-not $i -or -not $i.Text.Trim()) { continue }                        
                    '<maml:para>'
                    [Security.SecurityElement]::Escape($i.Text)
                    '</maml:para>'
                }
                '</dev:remarks>'
                '</command:example>'
            }
            '</command:examples>'
        }
        #endregion Write Examples

 

        #region Write Inputs
        $WriteInputs = {
            if (-not $commandHelp.inputTypes) { return } # If there were no input types, return.


            '<command:inputTypes>' #*Open the inputTypes Tag.
            foreach ($in in $commandHelp.inputTypes[0].inputType) { #*Walk thru each type in help.
                '<command:inputType>'  
                    & $WriteType $in #*Write the type information (in an inputType tag).
                '</command:inputType>'
            }
            '</command:inputTypes>' #*Close the Input Types Tag.
        }
        #endregion Write Inputs

        #region Write Outputs
        $WriteOutputs = {
            if (-not $commandHelp.returnValues) { return } # If there were no return values, return.
            
            '<command:returnValues>' # *Open the returnValues tag
            foreach ($rt in $commandHelp.returnValues[0].returnValue) { # *Walk thru each return value
                '<command:returnValue>' 
                    & $WriteType $rt # *write the type information (in an returnValue tag)
                '</command:returnValue>'
            }
            '</command:returnValues>' #*Close the returnValues tag
        }
        #endregion Write Outputs

        #region Write Notes
        $WriteNotes = {
            if (-not $commandHelp.alertSet) { return } # If there were no notes, return.
            "<maml:alertSet><maml:title></maml:title>" #*Open the alertSet tag and emit an empty title
            foreach ($note in $commandHelp.alertSet[0].alert) { #*Walk thru each note
                "<maml:alert><maml:para>"                    
                    $([Security.SecurityElement]::Escape($note.Text)) #*Put each note in a maml:alert element
                "</maml:para></maml:alert>"
            } 
            "</maml:alertSet>" #*Close the alertSet tag
        }
        #endregion Write Notes

        #region Write Syntax
        $WriteSyntax = {
            if (-not $cmdInfo.ParameterSets) { return } # If this command didn't have parameters, return
            
            "<command:syntax>" #*Open the syntax tag
            foreach ($syn in $cmdInfo.ParameterSets) {#*Walk thru each parameter set
                "<command:syntaxItem><maml:name>$($cmdInfo.Name)</maml:name>"  #*Create a syntaxItem tag, with the name of the command.
                foreach ($param in $syn.Parameters) { 
                    #* Skip parameters that are not directly declared (e.g. -ErrorAction)
                    if (-not $cmdMd.Parameters.ContainsKey($param.Name))  { continue } 
                    & $WriteParameter #* Write help for each parameter
                }
                "</command:syntaxItem>" #*Close the syntax item tag
            }
            "</command:syntax>"#*Close the syntax tag
            
        }
        #endregion Write Syntax

        #region Write Links
        $WriteLinks = {
            # If the command didn't have any links, return.
            if (-not $commandHelp.relatedLinks.navigationLink) { return }
            
            '<maml:relatedLinks>' #* Open a related Links tag
            foreach ($l in $commandHelp.relatedLinks.navigationLink) { #*Walk thru each link
                $linkText, $LinkUrl = "$($l.linkText)".Trim(), "$($l.Uri)".Trim() # and write it's tag.
                '<maml:navigationLink>'
                    "<maml:linkText>$linkText</maml:linkText>"
                    "<maml:uri>$LinkUrl</maml:uri>"
                '</maml:navigationLink>'
            }
            '</maml:relatedLinks>' #* Close the related Links tag
        }    
        #endregion Write Links


        #- - - Now that we've declared all of these little ScriptBlock parts, we'll put them in a list in the order they'll run.
        $WriteMaml = $writeCommandDetails, $writeSyntax,$WriteCommandParameters,$WriteInputs,$writeOutputs, $writeNotes, $WriteExamples, $writeLinks
        #- - -
    }
    
    process {
        
        if ($PSCmdlet.ParameterSetName -eq 'ByName') { # If we're getting comamnds by name,
            $CommandInfo = @(foreach ($n in $name) { 
                $ExecutionContext.InvokeCommand.GetCommands($N,'Function,Cmdlet', $true) # find each command (treating Name like a wildcard).
            })
        }


        if ($PSCmdlet.ParameterSetName -eq 'ByModule') { # If we're getting commands by module
            $CommandInfo = @(foreach ($m in $module) {  # find each module
                (Get-Module -Name $m).ExportedCommands.Values # and get it's exports.
            })
        }


        $filteredCmds = @(foreach ($ci in $CommandInfo) { # Filter the list of commands
            if ($ci -is [Management.Automation.AliasInfo] -or # (throw out aliases and applications).
                $ci -is [Management.Automation.ApplicationInfo]) { continue }
            $ci 
        })
         
        if ($filteredCmds) { 
            $null = $allCommands.AddRange($filteredCmds)
        }
    }
    
    end {
        $c, $t, $id, $maml = # Create some variables for our progress bar,
        0, $allCommands.Count, [Random]::new().Next(), [Text.StringBuilder]::new('<helpItems schema="maml">') # and initialize our MAML.
        
        foreach ($cmdInfo in $allCommands) { # Walk thru each command.
            $commandHelp = $null
            $c++
            $p = $c * 100 / $t
            Write-Progress 'Converting to MAML' "$cmdInfo [$c of $t]" -PercentComplete $p -Id $id # Write a progress message
            $commandHelp = $cmdInfo | Get-Help # get it's help
            $cmdMd = [Management.Automation.CommandMetaData]$cmdInfo # get it's command metadata
            if (-not $commandHelp -or $commandHelp -is [string]) { # (error if we couldn't Get-Help)
                Write-Error "$cmdInfo Must have a help topic to convert to MAML"
                return
            }                
            $verb, $noun  = $cmdInfo.Name -split "-" # and split out the noun and verb.
            


            # Now we're ready to run all of those script blocks we declared in begin.
            # All we need to do is append the command node, run each of the script blocks in $WriteMaml, and close the node.
            $mamlCommand = 
                "<command:command
                    xmlns:maml='http://schemas.microsoft.com/maml/2004/10'
                    xmlns:command='http://schemas.microsoft.com/maml/dev/command/2004/10'
                    xmlns:dev='http://schemas.microsoft.com/maml/dev/2004/10'>
                    $(foreach ($_ in $WriteMaml) { & $_ })
                </command:command>"

            $null = $maml.AppendLine($mamlCommand)
        }

        Write-Progress "Exporting Maml" " " -Completed -Id $id # Then we indicate we're done,
        $null = $maml.Append("</helpItems>") # close the opening tag.
        $mamlAsXml = [xml]"$maml" # and convert the whole thing to XML.
        if (-not $mamlAsXml) { return }  # If we couldn't, return.
        
        
        if ($XML) { return $mamlAsXml } # If we wanted the XML, return it.

        
        $strWrite = [IO.StringWriter]::new() # Now for a little XML magic:


        # If we create a [IO.StringWriter], we can save it as pretty or compacted XML.
        $mamlAsXml.PreserveWhitespace = $Compact # Oddly enough, if we're compacting we're setting preserveWhiteSpace to true, which in turn strips all of the whitespace except that inside of your nodes.
        $mamlAsXml.Save($strWrite) # Anyways, we can save this to the string writer, and it will either make our XML perfectly balanced and indented or compact and free of most whitespace.
        # Unfortunately, it will not get it's encoding declaration "right". This is because $strWrite is Unicode, and in most cases we'll want our XML to be UTF8.
        # The next step of the pipeline needs to convert it as it is saved, which is as easy as | Out-File -Encoding UTF8.
        "$strWrite".Replace('<?xml version="1.0" encoding="utf-16"?>','<?xml version="1.0" encoding="utf-8"?>') 
        $strWrite.Close()
        $strWrite.Dispose()
    }
} 
#.ExternalHelp HelpOut-Help.xml
function Get-MarkdownHelp {
    <#
    .SYNOPSIS
        Gets Markdown Help
    .DESCRIPTION
        Gets Help for a given command, in Markdown
    .EXAMPLE
        Get-MarkdownHelp Get-Help
    .LINK
        Save-MarkdownHelp
    .OUTPUTS
        [string]
 
        The documentation for a single command, in Markdown.
    #>

    param(
    # The name of the specified command or concept.
    [Parameter(Position=0, ValueFromPipelineByPropertyName)]
    [ValidateNotNullOrEmpty()]
    [string]
    $Name,

    # If set, will generate a markdown wiki. Links will be relative to the current path, and will not include the .md extensions
    [switch]
    $Wiki,

    # If set, will interlink documentation as if it were for GitHub pages, beneath a given directory
    [Alias('GitHubPageRoot')]    
    [string]
    $GitHubDocRoot,

    # If provided, will rename the help topic before getting markdown.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string]
    $Rename,

    # The order of the sections.
    # If not provided, this will be the order they are defined in the formatter.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string[]]
    $SectionOrder,

    # If set, will not enumerate valid values and enums of parameters.
    [Parameter(ValueFromPipelineByPropertyName)]
    [switch]
    $NoValidValueEnumeration
    )

    process
    {
        $getHelp = @{name=$Name}
        $myParams= @{} + $PSBoundParameters        
        $gotHelp = Get-Help @getHelp 
        if (-not $gotHelp) {
            Write-Error "Could not get help for $name"
            return
        }
        $gotHelp |
            & { process {
                    $in = $_
                    if ($in -is [string]) {
                        $in
                    } else {
                        $helpObj = $_
                        if ($Rename) {
                            $helpObj | Add-Member NoteProperty Rename $Rename -Force
                        }
                        $helpObj.pstypenames.clear()
                        $helpObj.pstypenames.add('PowerShell.Markdown.Help')
                        if ($SectionOrder) {
                            $helpObj | Add-Member NoteProperty SectionOrder $SectionOrder -Force    
                        }
                        $helpObj | Add-Member NoteProperty WikiLink ($Wiki -as [bool]) -Force
                        if ($myParams.ContainsKey("GitHubDocRoot")) {
                            $helpObj | Add-Member NoteProperty DocLink $GitHubDocRoot -Force
                        }
                        $helpObj | Add-Member NoteProperty NoValidValueEnumeration $NoValidValueEnumeration -Force
                        $helpObj
                    }
                } 
            }        
    }

        

}
 #requires -version 3.0
function Get-ScriptReference
{
    <#
    .Synopsis
        Gets a script's references
    .Description
        Gets the external references of a given PowerShell command. These are the commands the script calls, and the types the script uses.
    .Example
        Get-Command Get-ScriptReference | Get-ScriptReference
    #>

    [CmdletBinding(DefaultParameterSetName='FilePath')]
    param(
    # The path to a file
    [Parameter(Mandatory=$true,Position=0,ParameterSetName='FilePath',ValueFromPipelineByPropertyName=$true)]
    [Alias('Fullname')]
    [string[]]
    $FilePath,

    # One or more PowerShell ScriptBlocks
    [Parameter(Mandatory=$true,Position=0,ParameterSetName='ScriptBlock',ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [Alias('Definition')]
    [ScriptBlock[]]
    $ScriptBlock,

    # If set, will recursively find references.
    [switch]
    $Recurse
    )


    begin {
        # Let's declare some collections we'll need:
        $allFiles = [Collections.ArrayList]::new() # * A list of all files (if any are piped in)
        $LookedUpCommands = @{} # * The commands we've already looked up (to save time)
    }
    process {
        
        #region Process Piped in Files
        if ($PSCmdlet.ParameterSetName -eq 'FilePath') { # If we're piping in files,
            $allFiles.AddRange($FilePath) # add them to the list and process them in the end,
            return # and stop processing for good measure.
        }
        #endregion Process Piped in Files
                
        #region Get the Script References
        
        # To start off with, take all of the scripts passed in and put them in a queue.
        $scriptBlockQueue = [Collections.Generic.Queue[ScriptBlock]]::new($ScriptBlock)
        $resolvedCmds = @{} # Then create a hashtable to store the resolved references
        $alreadyChecked = [Collections.Generic.List[ScriptBlock]]::new() # and a list of all of the ScriptBlock's we've already taken a look at.

        # Now it's time for some syntax trickery that should probably be explained.
        
        
        # We're going to want to be able to recursively find references too.
        # By putting this in a queue, we've already done part of the work,
        # because we can just enqueue the nested commands.
        # However, we also want to know _which nested command had which references_
        # This means we have to collect all of the references as we go,
        # and output them in a different way if we're running recursively.


        # Got all that?
        
        
        # First, we need a tracking variable
        $CurrentCommand = '.' # for the current command.

        # Now the syntax trickery: We put the do loop inside of a lambda running in our scope (. {}).
        # This gives us all of our variables, but lets the results stream to the pipeline.
        # This is actually pretty important, since this way our tracking variable is accurate when we're outputting the results.
        
        # Now that we understand how it works, let's get to:
        
        #region Process the Queue of Script Blocks
        
        . { 
            $alreadyChecked = [Collections.ArrayList]::new()
            do { 
                $scriptBlock = $scriptBlockQueue.Dequeue()                
                if ($alreadyChecked -contains $scriptBlock) { continue } 
                $null=  $alreadyChecked.Add($ScriptBlock)
                $foundRefs = $Scriptblock.Ast.FindAll({
                    param($ast) 
                    $ast -is [Management.Automation.Language.CommandAst] -or 
                    $ast -is [Management.Automation.Language.TypeConstraintAst] -or 
                    $ast -is [Management.Automation.Language.TypeExpressionAst]
                }, $true)


                $cmdRefs = [Collections.ArrayList]::new()
                $cmdStatements = [Collections.ArrayList]::new()
                $typeRefs = [Collections.ArrayList]::new()

                foreach ($ref in $foundRefs) {
                    if ($ref -is [Management.Automation.Language.CommandAst]) {                    
                        $null = $cmdStatements.Add($ref)
                        if (-not $ref.CommandElements) { continue } 
                        $theCmd = $ref.CommandElements[0]
                        if ($theCmd.Value) {
                            
                            if (-not $LookedUpCommands[$theCmd.Value]) {
                                $LookedUpCommands[$thecmd.Value] = $ExecutionContext.InvokeCommand.GetCommand($theCmd.Value, 'Cmdlet, Function, Alias')
                            }
                            if ($cmdRefs -notcontains $LookedUpCommands[$theCmd.Value]) {
                                $null = $cmdRefs.Add($LookedUpCommands[$thecmd.Value])                            
                            }
                        } else {
                            # referencing a lambda, leave it alone for now
                        }
                    } elseif ($ref.TypeName) {
                        $refType = $ref.TypeName.Fullname -as [type]
                        if ($typeRefs -notcontains $refType) {
                            $null = $typeRefs.Add($refType)
                        }                        
                    }
                }


                [PSCustomObject][Ordered]@{
                    Commands = $cmdRefs.ToArray()
                    Statements = $cmdStatements.ToArray()
                    Types = $typeRefs.ToArray()
                }

                

            if ($Recurse) {
                $uniqueCmdRefs | 
                    & { process {
                        if ($resolvedCmds.ContainsKey($_.Name)) { return }
                        $nextScriptBlock = $_.ScriptBlock
                        if (-not $nextScriptBlock -and $_.ResolvedCommand.ScriptBlock)  {
                            $nextScriptBlock = $_.ResolvedCommand.ScriptBlock
                        }
                        if ($nextScriptBlock) { 
                            $scriptBlockQueue.Enqueue($nextScriptBlock)
                            $resolvedCmds[$_.Name] = $true
                        }
                    } }                 
            }                
        } while ($ScriptBlockQueue.Count) } | 
        #endregion Process the Queue of Script Blocks
        #region Handle Each Output
            & { 
                begin {
                    $refTable = @{}
                }
                process {
                    if (-not $Recurse) { return $_ } 
                }
            }
        #endregion Handle Each Output
        #endregion Get the Script References
                    
    }

    end {
        $myParams = @{} + $PSBoundParameters
        if (-not $allFiles.Count) { return }
        $c, $t, $id = 0, $allFiles.Count, $(Get-Random)
        foreach ($file in $allFiles) {
            $c++ 
            $resolvedFile=  try { $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($file)} catch { $null }
            if (-not $resolvedFile) { continue } 
            $resolvedFile = [IO.FileInfo]"$resolvedFile"
            if (-not $resolvedFile.Name) { continue }
            if (-not $resolvedFile.Length) { continue }
            if ('.ps1', '.psm1' -notcontains $resolvedFile.Extension) { continue }   
            $p = $c * 100 / $t
            $text = [IO.File]::ReadAllText($resolvedFile.FullName)
            $scriptBlock= [ScriptBlock]::Create($text)
            Write-Progress "Getting References" " $($resolvedFile.Name) " -PercentComplete $p -Id $id
            if (-not $scriptBlock) { continue }
            
            Get-ScriptReference -ScriptBlock $scriptBlock |
                & { process {
                    $_.psobject.properties.add([Management.Automation.PSNoteProperty]::new('FileName',$resolvedFile.Name))
                    $_.psobject.properties.add([Management.Automation.PSNoteProperty]::new('FilePath',$resolvedFile.Fullname))
                    $_.pstypenames.add('HelpOut.Script.Reference')
                    $_  
                } }                

            Write-Progress "Getting References" " " -Completed -Id $id
        }
    }
} 
#.ExternalHelp HelpOut-Help.xml
function Get-ScriptStory
{
    <#
    .Synopsis
        Gets a Script's story
    .Description
        Gets the Script's "Story"
 
        Script Stories are a simple markdown summary of all single-line comments within a script (aside from those in the param block).
    .Example
        Get-Command Get-ScriptStory | Get-ScriptStory
    .Notes
         
    #>

    [CmdletBinding(DefaultParameterSetName='ScriptBlock')]
    param(
    # A script block
    [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true,ParameterSetName='ScriptBlock')]
    [ScriptBlock]
    $ScriptBlock,

    # A block of text
    [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ParameterSetName='ScriptText')]
    [Alias('ScriptContents', 'Definition')]
    [string]
    $Text,
    
    # The friendly names of code regions or begin,process, or end blocks.
    [Collections.IDictionary]
    $RegionName = @{
        begin = "Before any input"
        process = "On Each Input"
        end = "After all input"
    },
    
    [int]
    $HeadingSize = 2)

    process {
        function foo($x, $y) {
            # Documentation should be ignored
        }

        # First, we want to convert any text input to -ScriptBlock.
        if ($PSCmdlet.ParameterSetName -eq 'ScriptText') {
            $ScriptBlock = [ScriptBlock]::Create($Text)
        }

        # Next, we tokenize the script and force it into an array.
        $tokens = @([Management.Automation.PSParser]::Tokenize("$ScriptBlock", [ref]$null))
        # We need to keep track of how many levels of regions we're in, so create a $RegionStack.
        $regionStack = [Collections.Stack]::new()
        # We'll also want to make a StringBuilder (because it will be faster).
        $sb= [text.stringbuilder]::new()
        # Last but not least, we'll want to keep track of a block depth, so initialize that to zero.
        $blockDepth = 0 
        #region Walk Thru Tokens
        for ($i =0; $i -lt $tokens.Length; $i++) {
            
            # As we pass GroupStarts and GroupEnds, nudge the block depth.
            if ($tokens[$i].Type -eq 'GroupStart') { $blockDepth++ }
            if ($tokens[$i].Type -eq 'GroupEnd') { $blockDepth-- } 

            #region Handle natural regions
            
            # In addition to any regions specified in documentation,
            # we can treat the begin, process, and end blocks as effective regions.
            if ($tokens[$i].Type -eq 'keyword' -and 
                'begin', 'process', 'end' -contains $tokens[$i].content -and 
                $blockDepth -le 1 ) {
                # When we encounter one of these regions, pop the region stack
                if ($regionStack.Count) { $null = $regionStack.Pop() }
                # and push the current region.
                $null =$regionStack.Push($tokens[$i].Content)


                # Generate the header, which consists of:
                $keywordHeader = 
                    # a newline,
                    [Environment]::NewLine + 
                    # N Markdown headers,
                    ('#' * ([Math]::Min(6, $regionStack.Count + $HeadingSize - 1))) + ' ' +
                    # the friendly name for the region (or just it's content),
                    $(if ($RegionName[$tokens[$i].Content]) {
                        $RegionName[$tokens[$i].Content]
                    } else { 
                        $tokens[$i].Content
                    }) +
                    # and another newline.
                    [Environment]::NewLine

                # Then, append the header.
                $null = $sb.Append($keywordHeader)
                continue
            }
            #endregion Handle natural regions

            #region Skip Parameter Block

            # We don't want all of the documentation.


            # Specifically, we want to avoid any parameter documentation and nested functions.
            # To do this, we need to notice the param and function keyword when it shows up.
            if ($tokens[$i].Type -eq 'keyword' -and 'param', 'function' -contains $tokens[$i].Content) {
                
                
                # Once we've found it, we advance until we find the next GroupStart.
                $j = $i + 1  
                while ($tokens[$j].Type -ne 'GroupStart') { $j++ }

                
                $skipGroupCount = 1
                if ($tokens[$j].Content -eq '(' -and  # If the GroupStart was an open paranthesis
                    $tokens[$i].Content -eq 'function'# and we're dealing with a nested function,
                ) {
                    $skipGroupCount = 2 # we're going to need to this next bit twice.
                }


                foreach ($n in 1..$skipGroupCount) {
                    # Look for the GroupStart.
                    while ($tokens[$j].Type -ne 'GroupStart') { $j++ }                
                    # Then we set a variable to track depth
                    $depth = 0  
                    do {
                        # and walk thru the tokens
                        if ($tokens[$j].Type -eq 'GroupStart') { $depth++ }
                        if ($tokens[$j].Type -eq 'GroupEnd') { $depth-- }
                        $j++
                    } while ($depth -and $tokens[$j]) # until the depth is 0 again.
                }

                
                $i = $j # Finally we set the iterator to current position (thus skipping the param block).
            }
            #endregion Skip Parameter Block

            #region Check for Paragraph Breaks

            # Next we need to check for paragraph breaks.
            
            if ($i -ge 2 -and
                $tokens[$i].Type -eq 'Newline' -and # If the current token is a newline,
                $tokens[$i -1].Type -eq 'Newline')  # and the token before that was also a newline,
            {
                # then it's probably a paragraph break
                if ($i -ge 3 -and $tokens[$i - 2].Type -eq 'GroupEnd') 
                {
                    # (Unless it followed a GroupEnd).
                    continue
                }


                # When we encounter a paragraph break, output two newlines.
                $null = $sb.Append([Environment]::NewLine * 2)
            }
            #endregion Check for Paragraph Breaks

            #region Process Comments

            # At this point, we don't care about anything other than comments.
            # So if it's not a comment, continue past them.
            if ($tokens[$i].Type -ne 'Comment') { continue }
            $Comment = $tokens[$i].Content.Trim([Environment]::NewLine).Trim()  
            if ($Comment.StartsWith('<')) { # If it's a block comment,
                # make sure it's not a special-purpose block comment (like inline help).
                $trimmedComment = $comment.Trim('<#').Trim([Environment]::NewLine).Trim()
                if ('?', '.', '{','-','|' -contains $trimmedComment[0]) { # If it was,
                    continue  # continue on.
                }
                # If it wasn't, trim the block comment and newlines.
                $Comment = $Comment.Trim().Trim("><#").Trim([Environment]::NewLine)
            }
                        
            
            # We'll need to know if it's a region
            # so we'll use some fancy Regex to extract it's name
            # (and if it's an EndRegion or not).

            if ($Comment.Trim() -match '#(?<IsEnd>end){0,1}region(?<RegionName>.{1,})') {
                $thisRegionName = $Matches.RegionName.Trim()
                if ($Matches.IsEnd) {
                    # If it was an EndRegion, pop it off of the Region Stack.
                    $null = $regionStack.Pop()
                } else {
                    # If it wasn't, push it onto the Region Stack.
                    $null = $regionStack.Push($thisRegionName)
                    # Then, output it's name a markdown header,
                    # using the count of RegionStack to determine H1, H2, etc.
                    $regionContent = 
                        [Environment]::NewLine + 
                        ('#' * ([Math]::Min(6, $regionStack.Count + $HeadingSize - 1))) + ' '+ 
                        $(if ($RegionName[$thisRegionName]) {
                            $RegionName[$thisRegionName]
                        } else { 
                            $Matches.RegionName.Trim()
                        }) +
                        [Environment]::NewLine
                    $null = $sb.Append($regionContent)
                }


                # We still don't want the region name to become part of the story,
                # so continue to the next token.
                continue
            }


            # Whatever comment is left is new story content.
            $newStory = $Comment.TrimStart('#').Trim()
                        
            # If there's any content already,
            if ($sb.Length) {
                # before we put it into the string,
                $null = 
                    if ($sb[-1] -eq '.') {
                        # add a double space (after a period),
                        $sb.Append(' ')
                    } else {
                        # or a single space.
                        $sb.Append(' ')   
                    }                
            }
            
            $shouldHaveNewline = 
                $newStory.StartsWith('*') -or 
                $newStory.StartsWith('-') -or 
                ($lastStory -and ($lastStory.StartsWith('*') -or $lastStory.StartsWith('-')))
            if ($shouldHaveNewline) {
                $null = $sb.Append([Environment]::NewLine)
            }
            # Finally, append the new story content.
            $null = $sb.Append($newStory)
            #endregion Process Comments
        }


        #endregion Walk Thru Tokens
    
        
        # After everything is done, output the content of the string builder.
        "$sb"
    }
}

 
 
#.ExternalHelp HelpOut-Help.xml
function Install-MAML
{
    <#
    .Synopsis
        Installs MAML into a module
    .Description
        Installs MAML into a module.
         
        This generates a single script that:
        * Includes all commands
        * Removes their multiline comments
        * Directs the commands to use external help
         
        You should then include this script in your module import.
 
        Ideally, you should use the allcommands script
    .Example
        Install-MAML -Module HelpOut
    .Link
        Save-MAML
    .Link
        ConvertTo-MAML
    #>

    [OutputType([Nullable], [IO.FileInfo])]
    param(
    # The name of one or more modules.
    [Parameter(Mandatory=$true,Position=0,ParameterSetName='Module',ValueFromPipelineByPropertyName=$true)]
    [string[]]
    $Module,
    
    # If set, will refresh the documentation for the module before generating the commands file.
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [switch]
    $NoRefresh,

    # If set, will compact the generated MAML. This will be ignored if -Refresh is not passed, since no new MAML will be generated.
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [switch]
    $Compact,
   
    # The name of the combined script. By default, allcommands.ps1.
    [Parameter(Position=1,ValueFromPipelineByPropertyName=$true)]
    [string]
    $ScriptName = 'allcommands.ps1',

    # The root directories containing functions. If not provided, the function root will be the module root.
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [string[]]
    $FunctionRoot,

    # If set, the function roots will not be recursively searched.
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [switch]
    $NoRecurse,

    # The encoding of the combined script. By default, UTF8.
    [Parameter(Position=2,ValueFromPipelineByPropertyName=$true)]
    [ValidateNotNull()]
    [Text.Encoding]
    $Encoding = [Text.Encoding]::UTF8,

    # A list of wildcards to exclude. This list will always contain the ScriptName.
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [string[]]
    $Exclude,

    # If set, the generate MAML will not contain a version number.
    # This slightly reduces the size of the MAML file, and reduces the rate of changes in the MAML file.
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [Alias('Unversioned')]
    [switch]
    $NoVersion,

    # If provided, will save the MAML to a different directory than the current UI culture.
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [Globalization.CultureInfo]
    $Culture,

    # If set, will return the files that were generated.
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [switch]
    $PassThru
    )

    process {        
        if ($ScriptName -notlike '*.ps1') { # First, let's check that the scriptname is a .PS1.
            $ScriptName += '.ps1' # If it wasn't, add the extension.
        }

        $Exclude += $ScriptName # Then, add the script name to the list of exclusions.

        if (-not $Culture) { # If no culture was specified,
            $Culture = [Globalization.CultureInfo]::CurrentUICulture # use the current UI culture.
        }

        foreach ($m in $Module) { 
            $theModule = Get-Module $m # Resolve the module.
            if (-not $theModule) { continue }  # If we couldn't, continue to the next.
            $theModuleRoot =  $theModule | Split-Path # Find the module's root.
            
            if ($PSBoundParameters.FunctionRoot) { # If we provided a function root parameter
                $functionRoot = foreach ($f in $FunctionRoot) { # then turn each root into an absolute path.
                    if ([IO.File]::Exists($F)) {
                        $f
                    } else {
                        Join-Path $theModuleRoot $f
                    }
                }
            } else {
                $FunctionRoot = "$theModuleRoot" # otherwise, just use the module root.
            }
            
            $fileList = @(foreach ($f in $FunctionRoot) { # Walk thru each function root.
                Get-ChildItem -Path $f -Recurse:$(-not $Recurse) -Filter *.ps1 | # recursively find all .PS1s
                    & { process {
                        if ($_.Name -notlike '*-*' -or $_.Name -like '*.*.*') { return  }  
                        foreach ($ex in $Exclude) { 
                            if ($_.Name -like $ex) { return } 
                        }
                        return $_
                    } }
            })

            #region Save the MAMLs
            if (-not $NoRefresh) { # If we're refreshing the MAML,
                $saveMamlCmd =  # find the command Save-MAML
                    if ($MyInvocation.MyCommand.ScriptBlock.Module) {
                        $MyInvocation.MyCommand.ScriptBlock.Module.ExportedCommands['Save-MAML']
                    } else {
                        $ExecutionContext.SessionState.InvokeCommand.GetCommand('Save-MAML', 'Function')
                    }
                $saveMamlSplat = @{} + $PSBoundParameters # and pick out the parameters that this function and Save-MAML have in common.
                foreach ($k in @($saveMamlSplat.Keys)) {
                    if (-not $saveMamlCmd.Parameters.ContainsKey($k)) {
                        $saveMamlSplat.Remove($k)
                    }
                }
                $saveMamlSplat.Module = $m # then, set the module
                Save-MAML @saveMamlSplat # and call Save-MAML
            }
            #endregion Save the MAMLs

            #region Generate the Combined Script
            # Prepare a regex to find function definitions.
            $regex = [Regex]::new('
                (?<![-\s\#]{1,}) # not preceeded by a -, or whitespace, or a comment
                function # function keyword
                \s{1,1} # a single space or tab
                (?<Name>[^\-]{1,1}\S+) # any non-whitespace, starting with a non-dash
                \s{0,} # optional whitespace
                [\(\{] # opening parenthesis or brackets
'
, 'MultiLine,IgnoreCase,IgnorePatternWhitespace', '00:00:05')

            
            $newFileContent = # We'll assign new file content by
                foreach ($f in $fileList) { # walking thru each file.
                    $fCmd = $ExecutionContext.SessionState.InvokeCommand.GetCommand($f.FullName, 'ExternalScript')
                    $fileContent = $fCmd.ScriptBlock # and read it as a string.
                    $start = 0
                    do { 
                        $matched = $regex.Match($fileContent,$start) # See if we find a functon.
                        if ($matched.Success) { # If we found one,
                            $insert = ([Environment]::NewLine + "#.ExternalHelp $M-Help.xml" + [Environment]::NewLine) # insert a line for help.
                            $fileContent = if ($matched.Index) {
                                $fileContent.Insert($matched.Index - 1, $insert)
                            } else {
                                $insert + $fileContent
                            }
                            $start += $matched.Index + $matched.Length
                            $start += $insert.Length # and update our starting position.
                        }        
                        # Keep doing this until we've reached the end of the file or the end of the matches.
                    } while ($start -le $filecontent.Length -and $matched.Success) 
  
                    # Then output the file content.
                    $fileContent
                }
            
            # Last but not least, we
            $combinedCommandsPath = Join-Path $theModuleRoot $ScriptName # determine the path for our combined commands file.

            "### DO NOT EDIT THIS FILE DIRECTLY ###" | Set-Content -Path $combinedCommandsPath -Encoding $Encoding.HeaderName.Replace('-','') # add a header
            [IO.File]::AppendAllText($combinedCommandsPath, $newFileContent, $Encoding) # and add our content.
            #endregion Generate the Combined Script

            if ($PassThru) {
                Get-Item -Path $combinedCommandsPath
            }
        }
    }
}
 
#.ExternalHelp HelpOut-Help.xml
function Measure-Help
{
    <#
    .Synopsis
        Determines the percentage of documentation
    .Description
        Determines the percentage of documentation in a given script
    .Example
        dir -Filter *.ps1 | Measure-Help
    .EXAMPLE
        Get-Command -Module HelpOut | Measure-Help
    .Example
        Measure-Help {
            # This script has some documentation, and then a bunch of code that literally does nothing
            $null = $null # The null equivilancy
            $null * 500 # x times nothing is still nothing
            $null / 100 # Nothing out of 100
        } | Select-Object -ExpandProperty PercentageDocumented
    .LINK
        Get-Help
    #>
    
    [CmdletBinding(DefaultParameterSetName='FilePath')]
    param(
    # The path to the file
    [Parameter(Mandatory,ValueFromPipelineByPropertyName,Position=0,ParameterSetName='FilePath')]
    [Alias('Fullname')]
    [string]
    $FilePath,

    # A PowerShell script block
    [Parameter(Mandatory,ParameterSetName='ScriptBlock',ValueFromPipelineByPropertyName)]
    [ScriptBlock]
    $ScriptBlock,

    # The name of the script being measured.
    [Parameter(ParameterSetName='ScriptBlock',ValueFromPipelineByPropertyName)]
    [string]
    $Name
    )

    begin {
        $fileList = New-Object Collections.ArrayList
        $ScriptBlockList = New-Object Collections.ArrayList
        $NameList = New-Object Collections.ArrayList

        filter OutputDocRatio {
            $scriptText = $_
            $scriptToken = [Management.Automation.PSParser]::Tokenize($scriptText, [ref]$null)

            # A quick tight little loop to sum
            # the lengths of different types in one
            # pass (Group-Object would work, but would be slower)
            $commentLength= 0
            $otherLength = 0
            $blockCommentLength = 0
            $inlineCommentLength  = 0
            $blockComments   = @()
            $inlineComments  = @()
            $totalLength = 0 
            foreach ($token in $ScriptToken) {
                $totalLength+=$token.Length
                if ($token.Type -eq 'Comment') {
                    if ($token.Content.StartsWith('<#')) {
                        $blockComments+=$token
                        $blockCommentLength += $token.Length
                    } else {
                        $inlineComments+=$token
                        $inlineCommentLength += $token.Length
                    }
                    $commentLength+=$token.Length
                } else {
                    $otherLength+=$token.Length
                }
            }
        
            # The percent is easy to calculate
            $percent =$commentLength * 100 / $totalLength
            @{
                CommentLength       = $commentLength
                TokenLength         = $otherLength
                CommentPercent      = $percent
                BlockComments       = $blockComments
                BlockCommentLength  = $blockCommentLength
                InlineComments      = $inlineComments
                InlineCommentLength = $inlineCommentLength
            }
            
        }        
    }

    process {
        if ($PSCmdlet.ParameterSetName -eq 'FilePath') {
            $fileList.AddRange(@($FilePath))
        } elseif ($PSCmdlet.ParameterSetName -eq 'ScriptBlock') {
            $null = $ScriptBlockList.Add($ScriptBlock)
            $null = $NameList.Add($Name)
        }
    }

    end {
        if ($ScriptBlockList.Count) {
            $scriptBlockIndex =0 
            foreach ($sb in $ScriptBlockList) {
                [PSCustomObject]([Ordered]@{
                    PSTypeName = "Documentation.Percentage"
                    Name = $NameList[$scriptBlockIndex]
                    ScriptBlock = $sb
                } + ($sb | OutputDocRatio))
                $scriptBlockIndex++
            }
        }

        if ($fileList.Count) {
            foreach ($f in $fileList) {
                $RF = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($F)
                if (-not $rf) { continue }
                $fileItem = Get-Item -LiteralPath $RF
                $sb = $null
                $sb = try {
                    [ScriptBlock]::Create([IO.File]::ReadAllText($RF))
                } catch {
                    $null 
                }

                if ($sb) {
                    [PSCustomObject]([Ordered]@{
                        PSTypeName = "File.Documentation.Percentage"
                        Name = $fileItem.Name
                        FilePath = "$rf"
                        ScriptBlock = $sb
                    } + ($sb | OutputDocRatio))
                }
            }
        }
    }
} 
#.ExternalHelp HelpOut-Help.xml
function Save-MAML
{
    <#
    .Synopsis
        Saves a Module's MAML
    .Description
        Generates a Module's MAML file, and then saves it to the appropriate location.
    .Link
        Get-MAML
    .Example
        Save-Maml -Module HelpOut
    .Example
        Save-Maml -Module HelpOut -WhatIf
    .Example
        Save-Maml -Module HelpOut -PassThru
    #>

    [CmdletBinding(DefaultParameterSetName='CommandInfo',SupportsShouldProcess=$true)]
    [OutputType([Nullable])]
    param( 
    # The name of one or more modules.
    [Parameter(ParameterSetName='ByModule',ValueFromPipelineByPropertyName=$true)]
    [string[]]
    $Module,

    # If set, the generated MAML will be compact (no extra whitespace or indentation). If not set, the MAML will be indented.
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [switch]
    $Compact,
    
    # If provided, will save the MAML to a different directory than the current UI culture.
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [Globalization.CultureInfo]
    $Culture,

    # If set, the generate MAML will not contain a version number.
    # This slightly reduces the size of the MAML file, and reduces the rate of changes in the MAML file.
    [Alias('Unversioned')]
    [switch]
    $NoVersion,
    
    # If set, will return the files that were generated.
    [switch]
    $PassThru)

    begin {
        # First, let's cache a reference to Get-MAML
        $getMAML = 
            if ($MyInvocation.MyCommand.ScriptBlock.Module) {
                $MyInvocation.MyCommand.ScriptBlock.Module.ExportedCommands['Get-MAML']
            } else {
                $ExecutionContext.SessionState.InvokeCommand.GetCommand('Get-MAML', 'Function')
            }
    }

    process {
        if (-not $getMAML) { # If for whatever reason we don't have Get-MAML
            Write-Error "Could not Find Get-MAML" -Category ObjectNotFound -ErrorId Get-MAML.NotFound # error out.
            return
        }


        $c, $t, $id = 0, $Module.Length, [Random]::new().Next() 
        $splat = @{} + $PSBoundParameters # Copy our parameters
        foreach ($k in @($splat.Keys)) { # then strip out any parameter
            if (-not $getMAML.Parameters.ContainsKey($k)) { # that wasn't in Get-MAML.
                $splat.Remove($k)
            }
        }

        if (-not $Culture) { # If -Culture wasn't provided, use the current culture
            $Culture = [Globalization.CultureInfo]::CurrentCulture
        }

        #region Save the MAMLs
        foreach ($m in $Module) { # Walk thru the list of module names.
            $splat.Module = $m 
            if ($t -gt 1) {
                $c++
                Write-Progress 'Saving MAML' $m -PercentComplete $p  -Id $id
            }

            $theModule = Get-Module $m # Find the module
            if (-not $theModule) { continue } # (continue if we couldn't).
            $theModuleRoot = $theModule | Split-Path # Find the module's root,
            $theModuleCultureDir = Join-Path $theModuleRoot $Culture.Name # then find the culture folder.

            if (-not (Test-Path $theModuleCultureDir)) { # If that folder didn't exist,
                $null = New-Item -ItemType Directory -Path $theModuleCultureDir # create it.
            }
            
            $theModuleHelpFile = Join-Path $theModuleCultureDir "$m-Help.xml" # Construct the path to the module help file (e.g. en-us\Module-Help.xml)

            & $getMAML @splat | # Convert the module help to MAML,
                Set-Content -Encoding UTF8 -Path $theModuleHelpFile -Passthru: $PassThru # and write the file.
         }

        if ($t -gt 1) {
            Write-Progress 'Saving MAML' 'Complete' -Completed -Id $id
        }
        #endregion Save the MAMLs
    }
}
 
#.ExternalHelp HelpOut-Help.xml
function Save-MarkdownHelp
{
    <#
    .Synopsis
        Saves a Module's Markdown Help
    .Description
        Get markdown help for each command in a module and saves it to the appropriate location.
    .Link
        Get-MarkdownHelp
    .Example
        Save-MarkdownHelp -Module HelpOut # Save Markdown to HelpOut/docs
    .Example
        Save-MarkdownHelp -Module HelpOut -Wiki # Save Markdown to ../HelpOut.wiki
    #>

    param(
    # The name of one or more modules.
    [Parameter(ParameterSetName='ByModule',ValueFromPipelineByPropertyName=$true)]
    [string[]]
    $Module,

    # The output path.
    # If not provided, will be assumed to be the "docs" folder of a given module (unless -Wiki is specified)
    [Parameter(ValueFromPipelineByPropertyName)]
    [string]
    $OutputPath,

    # If set, will interlink documentation as if it were a wiki. Implied when -OutputPath contains 'wiki'.
    # If provided without -OutputPath, will assume that a wiki resides in a sibling directory of the module.
    [Parameter(ValueFromPipelineByPropertyName)]
    [switch]
    $Wiki,

    # If provided, will generate documentation for any scripts found within these paths.
    # -ScriptPath can be either a file name or a full path.
    # If an exact match is not found -ScriptPath will also check to see if there is a wildcard match.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string[]]
    $ScriptPath,

    # If provided, will replace parts of the names of the scripts discovered in a -ScriptDirectory beneath a module.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string[]]
    $ReplaceScriptName,

    # If provided, will replace parts of the names of the scripts discovered in a -ScriptDirectory beneath a module with a given Regex replacement.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string[]]
    $ReplaceScriptNameWith,

    # If set, will output changed or created files.
    [switch]
    $PassThru,

    # The order of the sections. If not provided, this will be the order they are defined in the formatter.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string[]]
    $SectionOrder,

    # One or more topic files to include.
    # Topic files will be treated as markdown and directly copied inline.
    # By default ```\.help\.txt$``` and ```\.md$```
    [Parameter(ValueFromPipelineByPropertyName)]
    [string[]]
    $IncludeTopic = @('\.help\.txt$', '\.md$'),

    # If set, will not enumerate valid values and enums of parameters.
    [Parameter(ValueFromPipelineByPropertyName)]
    [switch]
    $NoValidValueEnumeration,

    # A list of command types to skip.
    # If not provided, all types of commands from the module will be saved as a markdown document.
    [Parameter(ValueFromPipelineByPropertyName)]
    [Alias('SkipCommandTypes','OmitCommandType','OmitCommandTypes')]
    [Management.Automation.CommandTypes[]]
    $SkipCommandType
    )

    begin {
        # First, let's cache a reference to Get-MarkdownHelp
        $GetMarkdownHelp = 
            if ($MyInvocation.MyCommand.ScriptBlock.Module) {
                $MyInvocation.MyCommand.ScriptBlock.Module.ExportedCommands['Get-MarkdownHelp']
            } else {
                $ExecutionContext.SessionState.InvokeCommand.GetCommand('Get-MarkdownHelp', 'Function')
            }
    }

    process {        
        $getMarkdownHelpSplatBase = @{}
        if ($SectionOrder) {
            $getMarkdownHelpSplatBase.SectionOrder =$SectionOrder
        }
        if ($NoValidValueEnumeration) {
            $getMarkdownHelpSplatBase.NoValidValueEnumeration =$true
        }
        #region Save the Markdowns
        foreach ($m in $Module) { # Walk thru the list of module names.
            if ($t -gt 1) {
                $c++
                Write-Progress 'Saving Markdown' $m -PercentComplete $p  -Id $id
            }

            $theModule = Get-Module $m # Find the module
            if (-not $theModule) { continue } # (continue if we couldn't).
            $theModuleRoot = $theModule | Split-Path # Find the module's root,
            if (-not $psBoundParameters.OutputPath) {                
                $OutputPath = 
                    if ($Wiki) {
                        Split-Path $theModuleRoot | Join-Path -ChildPath "$($theModule.Name).wiki"
                    } else {
                        Join-Path $theModuleRoot "docs"                        
                    }                
            }
            
            if (-not (Test-Path $OutputPath)) {
                $null = New-Item -ItemType Directory -Path $OutputPath
            }

            $outputPathItem = Get-Item $OutputPath
            if ($outputPathItem -isnot [IO.DirectoryInfo]) {
                Write-Error "-OutputPath '$outputPath' must point to a directory"
                return
            }

            $myHelpParams = @{}
            if ($wiki) { $myHelpParams.Wiki = $true}
            else { $myHelpParams.GitHubDocRoot = $OutputPath | Split-Path}            

            foreach ($cmd in $theModule.ExportedCommands.Values) {
                $docOutputPath = Join-Path $outputPath ($cmd.Name + '.md')
                if ($SkipCommandType -and $SkipCommandType -contains $cmd.CommandType) {
                    continue
                }
                $getMarkdownHelpSplat = @{Name="$cmd"} + $getMarkdownHelpSplatBase
                if ($Wiki) { $getMarkdownHelpSplat.Wiki = $Wiki}
                else { $getMarkdownHelpSplat.GitHubDocRoot = "$($outputPath|Split-Path -Leaf)"}
                & $GetMarkdownHelp @getMarkdownHelpSplat| Out-String -Width 1mb | Set-Content -Path $docOutputPath -Encoding utf8
                if ($PassThru) {
                    Get-Item -Path $docOutputPath -ErrorAction SilentlyContinue
                }
            }

            if ($ScriptPath) {
                $childitems = Get-ChildItem -Path $theModuleRoot -Recurse
                foreach ($sp in $ScriptPath) {
                    $childitems |
                        Where-Object { 
                                $_.Name -eq $sp -or $_.FullName -eq $sp -or $_.Name -like $sp -or $_.FullName -like $sp
                        } |
                        Get-ChildItem |
                        Where-Object Extension -eq '.ps1' |
                        ForEach-Object {
                            $ps1File = $_
                            $getMarkdownHelpSplat = @{Name="$($ps1File.FullName)"} + $getMarkdownHelpSplatBase
                            $replacedFileName = $ps1File.Name
                            @(for ($ri = 0; $ri -lt $ReplaceScriptName.Length; $ri++) {
                                if ($ReplaceScriptNameWith[$ri]) {
                                    $replacedFileName = $replacedFileName -replace $ReplaceScriptName[$ri], $ReplaceScriptNameWith[$ri]
                                } else {
                                    $replacedFileName = $replacedFileName -replace $ReplaceScriptName[$ri]
                                }
                            })
                            $docOutputPath = Join-Path $outputPath ($replacedFileName + '.md')
                            $relativePath = $ps1File.FullName.Substring("$theModuleRoot".Length).TrimStart('/\').Replace('\','/')
                            $getMarkdownHelpSplat.Rename = $relativePath
                            if ($Wiki) { $getMarkdownHelpSplat.Wiki = $Wiki}
                            else { $getMarkdownHelpSplat.GitHubDocRoot = "$($outputPath|Split-Path -Leaf)"}
                            & $GetMarkdownHelp @getMarkdownHelpSplat| Out-String -Width 1mb | Set-Content -Path $docOutputPath -Encoding utf8
                            if ($PassThru) {
                                Get-Item -Path $docOutputPath -ErrorAction SilentlyContinue
                            }
                        }

                    
                }
            }

            if ($IncludeTopic) {
                Get-ChildItem -Path $theModuleRoot -Recurse -File |
                    ForEach-Object {
                        $fileInfo = $_
                        foreach ($inc in $IncludeTopic) {
                            if ($fileInfo.Name -match $inc) {
                                $replacedName = ($fileInfo.Name -replace $inc)
                                if ($replacedName -eq "about_$module") {
                                    $replacedName = 'README'
                                }
                                $dest = Join-Path $OutputPath ($replacedName + '.md')
                                if ($fileInfo.FullName -ne "$dest") {
                                    $fileInfo | Copy-Item -Destination $dest
                                }
                            }
                        }
                    }
            }
         }

        if ($t -gt 1) {
            Write-Progress 'Saving Markdown' 'Complete' -Completed -Id $id
        }
        #endregion Save the Markdowns
    }
}