Public/Convert-PSFunctionHelpToMarkdown.ps1

Function Convert-PSFunctionHelpToMarkdown
{
    <#
        .SYNOPSIS
        Converts PowerShell script help to Markdown documentation.
 
        .DESCRIPTION
        Converts PowerShell script help to Markdown documentation. This function is capable of either outputting data to console, file, or both simultaneously.
 
        By default the function will capture help metadata at a function-level, and will not capture any script-level data. This functionality can be controlled with the `-IncludeNestedFunctions` parameter. For capturing documentation in script files, please use `Convert-PSScriptHelpToMarkdown` instead.
 
        The function will generate warnings for any missing data fields in order to notify the operator and ensure completeness of documentation.
 
        .PARAMETER InputObject
        The name of the file to convert. Fully qualified and relative paths are both supported.
 
        If functions have been loaded into memory via dot-sourcing or module import, they may be passed via Get-Command or by name.
 
        .PARAMETER OutputPath
        The path to which to export the Markdown document. Folder paths and individual filenames are both supported.
 
        .PARAMETER HeaderGranularity
        Controls whether parameter names and examples are individually converted to Markdown headers. This setting will impact the appearance of the rendered text in addition to the metadata.
 
        .PARAMETER OutFile
        Controls whether an output file is generated.
 
        If used in conjunction with `-OutputPath`, a file can be generated in the desired destination.
 
        If used without `-OutputPath`, the files will be generated in the same directory in which the script files reside. If functions are ready from memory instead of from a file, the destination file will default to the present working directory of the active PowerShell session.
 
        .PARAMETER PassThru
        Returns output to the console in addition to producing a file.
 
        .PARAMETER Force
        Forces an overwrite of an existing file.
 
        .PARAMETER Append
        Appends to an existing file. Necessary when consolidating documentation from multiple files into one.
 
        .EXAMPLE
        # Generate documentation to the console for a single file
        Convert-PSFunctionHelpToMarkdown C:\Path\To\Script.ps1
 
        .EXAMPLE
        # Generate documentation to the console for a function that has been loaded into memory.
        Convert-PSFunctionHelpToMarkdown MyFunction
 
        .EXAMPLE
        # Generate documentation for a single file to a Markdown file with the same name and location, including any nested functions inside the file, and output to the console.
        Convert-PSFunctionHelpToMarkdown C:\Path\To\Script.ps1 -PassThru
 
        .EXAMPLE
        # Generate documentation for multiple files in a directory tree to individual Markdown files with the same name and location as the .ps1 files
        Get-ChildItem C:\Path\To\Functions\*.ps1 -Recurse | Convert-PSFunctionHelpToMarkdown -OutFile
 
        .EXAMPLE
        # Generate documentation for multiple files in a directory tree to individual Markdown files with the same name as the .ps1 files, but in a new folder
        Get-ChildItem C:\Path\To\Functions\*.ps1 -Recurse | Convert-PSFunctionHelpToMarkdown -OutFile -OutputPath C:\Path\To\Functions\Docs
 
        .EXAMPLE
        # Generate documentation for multiple files in a directory tree to a single Markdown file
        Get-ChildItem C:\Path\To\Functions\*.ps1 -Recurse | Convert-PSFunctionHelpToMarkdown -OutFile -OutputPath C:\Path\To\Functions\Consolidated.md -Force -Append
 
        .EXAMPLE
        # Generate documentation for a module to individual Markdown files with the same name as each function
        Get-Command -Module MyModule -CommandType Function | Convert-PSFunctionHelpToMarkdown -OutFile -OutputPath C:\Path\To\Docs
 
        .NOTES
        - Help data for dynamic parameters is not captured or converted. This is a known issue with Get-Help and not with this function. You can use examples that show dynamic parameters or move documentation of dynamic parameters to another section (such as the Description Field) in order to work around this limitation. [GitHub Issue](https://github.com/PowerShell/PowerShell/issues/6694)
        - This function does not support external help files at this time.
 
    #>


    [CmdletBinding(DefaultParameterSetName="__AllParameterSets")]
    PARAM
    (
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
        [ValidateNotNullOrEmpty()]
        [Alias("FullName")]
        $InputObject,

        [Parameter(Mandatory=$false, Position=1, ParameterSetName="OutFile")]
        [ValidateNotNullOrEmpty()]
        [String]$OutputPath,

        [Parameter(Mandatory=$false)]
        [ValidateSet("Coarse","Fine")]
        [String]$HeaderGranularity = "Fine",

        [Parameter(Mandatory=$false, ParameterSetName="OutFile")]
        [Switch]$OutFile
    )

    # Create dynamic parameters
    DynamicParam
    {
        IF ($PSCmdlet.ParameterSetName -eq "OutFile")
        {
            # Create runtine dictionary
            $ParamDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()

            # Define parameter attributes
            $Attribute  = [System.Management.Automation.ParameterAttribute]@{
                Mandatory        = $false
                ParameterSetName = "OutFile"
            }

            # Create attribute collection
            $AttributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]$Attribute

            # Create parameter object and add name, type, attribute collection
            $PassThruParameter = [System.Management.Automation.RuntimeDefinedParameter]::new('PassThru',[Switch],$AttributeCollection)
            $PassThruParameter.Value = $false
            $PSBoundParameters['PassThru'] = $PassThruParameter.Value

            # Add parameter to runtime dictionary
            $ParamDictionary.Add('PassThru',$PassThruParameter)

            # Create parameter object and add name, type, attribute collection
            $ForceParameter = [System.Management.Automation.RuntimeDefinedParameter]::new('Force',[Switch],$AttributeCollection)
            $ForceParameter.Value = $false
            $PSBoundParameters['Force'] = $ForceParameter.Value

            # Add parameter to runtime dictionary
            $ParamDictionary.Add('Force',$ForceParameter)

            # Create parameter object and add name, type, attribute collection
            $AppendParameter = [System.Management.Automation.RuntimeDefinedParameter]::new('Append',[Switch],$AttributeCollection)
            $AppendParameter.Value = $false
            $PSBoundParameters['Append'] = $AppendParameter.Value

            # Add parameter to runtime dictionary
            $ParamDictionary.Add('Append',$AppendParameter)

            return $ParamDictionary
        }
    }

    #region BEGIN Block
    BEGIN
    {
        # Transfer Dynamic Parameters to runtime
        $Append   = $PSBoundParameters['Append']
        $PassThru = $PSBoundParameters['PassThru']
        $Force    = $PSBoundParameters['Force']

        # Locally scope ErrorActionPreference for predictable behavior of Try/Catch blocks inside the function
        $ErrorActionPreference = 'Stop'
    }
    #endregion BEGIN Block

    #region PROCESS Block
    PROCESS
    {
        # Create output variable
        $Results = [System.Collections.ArrayList]::new()

        #region Functions
        #region Functions: Gather Info
        # Get script and function documentation
        $FunctionDocumentation = Get-PSFunctionDocumentation $InputObject

        # Throw warning if function documentation is not found
        IF ($FunctionDocumentation -eq $null)
        {
            Write-Warning "Function documentation is null."
            Return
        }
        #endregion Functions: Gather Info

        FOREACH ($Function in $FunctionDocumentation)
        {
            # Declare variables
            $FunctionFileHeader  = ''
            $FunctionSyntax      = ''
            $FunctionSynopsis    = ''
            $FunctionDescription = ''
            $FunctionParameters  = ''
            $FunctionExamples    = ''
            $FunctionNotes       = ''

            #region Functions: File Info
            # Construct file header string
            $FunctionFileHeader += "$($Function.Name | Sort -Unique)`n" | ConvertTo-MarkdownHeader

            # Add to results
            $Results.Add("$FunctionFileHeader`n") | Out-Null
            #endregion Functions: File Info

            #region Functions: Syntax
            # Construct syntax string
            $FunctionSyntax += "Syntax" | ConvertTo-MarkdownHeader -Level 2
            $FunctionSyntax += "`n"
            $FunctionSyntax += $Function.Syntax | Convertto-MarkdownCodeBlock -Language PowerShell
            $FunctionSyntax += "`n"

            # Add to results
            $Results.Add($FunctionSyntax) | Out-Null
            #endregion Functions: Syntax

            #region Functions: Synopsis
            # Construct synopsis string
            IF ($Function.Synopsis)
            {
                $FunctionSynopsis += "Synopsis" | ConvertTo-MarkdownHeader -Level 2
                $FunctionSynopsis += "`n$($Function.Synopsis)`n"

                # Add to results
                $Results.Add($FunctionSynopsis) | Out-Null
            }
            #endregion Functions: Synopsis

            #region Functions: Description
            # Construct description string
            IF ($Function.Description)
            {
                $FunctionDescription += "Description" | ConvertTo-MarkdownHeader -Level 2
                $FunctionDescription += "`n$($Function.Description.Text)`n"

                # Add to results
                $Results.Add($FunctionDescription) | Out-Null
            }
            #endregion Functions: Description

            #region Functions: Parameters
            # Construct parameter string
            IF ($Function.Parameters)
            {
                $FunctionParameters += "Parameters" | ConvertTo-MarkdownHeader -Level 2
                $FunctionParameters += "`n"

                FOREACH ($Parameter in $Function.Parameters)
                {
                    SWITCH($HeaderGranularity)
                    {
                        "Coarse" {$FunctionParameters += "-$($Parameter.Name)" | Convertto-MarkdownCodeBlock -Inline | ConvertTo-MarkdownFormattedText -Bold ; $FunctionParameters += "`n"}
                        "Fine"   {$FunctionParameters += "-$($Parameter.Name)" | Convertto-MarkdownCodeBlock -Inline | ConvertTo-MarkdownHeader -Level 3}
                    }
                    $FunctionParameters += "`n"
                    $FunctionParameters += $Parameter.Description.Text
                    $FunctionParameters += "`n"
                    $FunctionParameters += "`n"
                }

                SWITCH($HeaderGranularity)
                {
                    "Coarse" {$FunctionParameters += "Parameter Details" | ConvertTo-MarkdownFormattedText -Bold}
                    "Fine"   {$FunctionParameters += "Parameter Details" | ConvertTo-MarkdownHeader -Level 3}
                }
                $FunctionParameters += "`n"
                $FunctionParameters += $Function.Parameters | Select Name,DefaultValue,Required,ParameterValue,Position,PipelineInput | ConvertTo-MarkdownTable

                # Add to results
                $Results.Add($FunctionParameters) | Out-Null
            }
            #endregion Functions: Parameters

            #region Functions: Examples
            # Construct examples string
            IF ($Function.Examples)
            {
                $FunctionExamples += "Examples" | ConvertTo-MarkdownHeader -Level 2
                $FunctionExamples += "`n"
                FOREACH ($Example in $Function.Examples)
                {
                    SWITCH($HeaderGranularity)
                    {
                        "Coarse" {$FunctionExamples += (Get-Culture).TextInfo.ToTitleCase($Example.Title.ToLower()) | ConvertTo-MarkdownFormattedText -Bold}
                        "Fine"   {$FunctionExamples += (Get-Culture).TextInfo.ToTitleCase($Example.Title.ToLower()) | ConvertTo-MarkdownHeader -Level 3}
                    }
                    $FunctionExamples += "`n"
                    $FunctionExamples += $Example.Description.ToString() | Convertto-MarkdownCodeBlock -Language PowerShell
                    $FunctionExamples += "`n"
                    $FunctionExamples += "`n"
                }
                "$($Function.Examples)`n"

                # Add to results
                $Results.Add($FunctionExamples) | Out-Null
            }
            #endregion Functions: Examples

            #region Functions: Notes
            # Construct notes string
            IF ($Function.Notes)
            {
                $FunctionNotes += "Notes" | ConvertTo-MarkdownHeader -Level 2
                $FunctionNotes += "`n$($Function.Notes)`n"

                # Add to results
                $Results.Add($FunctionNotes) | Out-Null
            }
            #endregion Functions: Notes
        }
        #endregion Functions

        #region Output Handling
        IF ($PSCmdlet.ParameterSetName -eq "OutFile")
        {
            IF ($OutputPath)
            {
                # Test whether output path is a file or directory
                SWITCH -Regex ($OutputPath)
                {
                    #region Output Handling: File
                    # Absolute regex madness
                    "\.\w\w$|\.\w\w\w$|\.\w\w\w\w$|\.markdown$"
                    {
                        $OutputFilename  = $OutputPath.Split('\')[-1]
                        $OutputDirectory = $OutputPath.Substring(0,$OutputPath.LastIndexOf('\'))
                    }
                    #endregion Output Handling: File
                    #region Output Handling: Folder
                    Default
                    {
                        $OutputFilename  = "$($FunctionDocumentation.Name.Split('.')[0]).md"
                        $OutputDirectory = $OutputPath
                    }
                    #endregion Output Handling: Folder
                }
            }
            ELSE
            #region Output Handling: Unspecified
            {
                TRY
                {
                    $OutputFilename  = "$($FunctionDocumentation.Filename.Split('\')[-1].Split('.')[0]).md"
                    $OutputDirectory = $FunctionDocumentation.Filename.Substring(0,$FunctionDocumentation.Filename.LastIndexOf('\'))
                }
                CATCH
                {
                    Write-Warning "Unable to determine function directory. Defaulting to present working directory: $PWD"
                    $OutputFilename = "$($FunctionDocumentation.Filename).md"
                    $OutputDirectory = $PWD
                }
            }
            #endregion Output Handling: Unspecified

            # Create path if it doesn't exist
            IF (!(Test-Path $OutputDirectory))
            {
                New-Item -Path $OutputDirectory -ItemType Directory | Out-Null
            }

            # Output file
            $Results | Out-File $OutputDirectory\$OutputFilename -Encoding ascii -Force:$Force -Append:$Append
        }
        #endregion Output Handling

        #region Results
        # Return results to the console if an output has not been specified or -Passthru has been selected
        IF(($PSCmdlet.ParameterSetName -ne "OutFile") -or $Passthru)
        {
            Return $Results
        }
        #endregion Results
    }
    #endregion PROCESS Block

    #region END Block
    END
    {
        # Nothing to see here. Move along.
    }
    #endregion END Block
}