Documentarian.ModuleAuthor.psm1

# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

#region Enums.Public

[Flags()] enum ProviderFlags {
  Registry = 0x01
  Alias = 0x02
  Environment = 0x04
  FileSystem = 0x08
  Function = 0x10
  Variable = 0x20
  Certificate = 0x40
  WSMan = 0x80
}

enum ParameterAttributeKind {
    DontShow
    Experimental
    HasValidation
    SupportsWildcards
    ValueFromPipeline
    ValueFromRemaining
}

#endregion Enums.Public

#region Classes.Public

class ParameterInfo {
  [string]$Name
  [string]$HelpText
  [string]$Type
  [string]$ParameterSet
  [string]$Aliases
  [bool]$Required
  [string]$Position
  [string]$Pipeline
  [bool]$Wildcard
  [bool]$Dynamic
  [bool]$FromRemaining
  [bool]$DontShow
  [ProviderFlags]$ProviderFlags

  ParameterInfo(
    [System.Management.Automation.ParameterMetadata]$param,
    [ProviderFlags]$ProviderFlags
  ) {
    $this.Name = $param.Name
    $this.HelpText = if ($null -eq $param.Attributes.HelpMessage) {
      '{{Placeholder}}'
    } else {
      $param.Attributes.HelpMessage
    }
    $this.Type = $param.ParameterType.FullName
    $this.ParameterSet = if ($param.Attributes.ParameterSetName -eq '__AllParameterSets') {
      '(All)'
    } else {
      $param.Attributes.ParameterSetName -join ', '
    }
    $this.Aliases = $param.Aliases -join ', '
    $this.Required = $param.Attributes.Mandatory
    $this.Position = if ($param.Attributes.Position -lt 0) {
      'Named'
    } else {
      $param.Attributes.Position
    }
    $this.Pipeline = 'ByValue ({0}), ByName ({1})' -f $param.Attributes.ValueFromPipeline, $param.Attributes.ValueFromPipelineByPropertyName
    $this.Wildcard = $param.Attributes.TypeId.Name -contains 'SupportsWildcardsAttribute'
    $this.Dynamic = $param.IsDynamic
    $this.FromRemaining = $param.Attributes.ValueFromRemainingArguments
    $this.DontShow = $param.Attributes.DontShow
    $this.ProviderFlags = $ProviderFlags
  }

  [string]ToMarkdown([bool]$showAll) {
    $sbMarkdown = [System.Text.StringBuilder]::new()
    $sbMarkdown.AppendLine("### -$($this.Name)")
    $sbMarkdown.AppendLine()
    $sbMarkdown.AppendLine($this.HelpText)
    $sbMarkdown.AppendLine()
    $sbMarkdown.AppendLine('```yaml')
    $sbMarkdown.AppendLine("Type: $($this.Type)")
    $sbMarkdown.AppendLine("Parameter Sets: $($this.ParameterSet)")
    $sbMarkdown.AppendLine("Aliases: $($this.Aliases)")
    $sbMarkdown.AppendLine()
    $sbMarkdown.AppendLine("Required: $($this.Required)")
    $sbMarkdown.AppendLine("Position: $($this.Position)")
    $sbMarkdown.AppendLine('Default value: None')
    $sbMarkdown.AppendLine("Accept pipeline input: $($this.Pipeline)")
    $sbMarkdown.AppendLine("Accept wildcard characters: $($this.Wildcard)")
    if ($showAll) {
      $sbMarkdown.AppendLine("Dynamic: $($this.Dynamic)")
      if ($this.Dynamic -and $this.ProviderFlags) {
        $ProviderName = if ($this.ProviderFlags -eq 0xFF) {
          'All'
        } else {
          $this.ProviderFlags.ToString()
        }
        $sbMarkdown.AppendLine("Providers: $ProviderName")
      }
      $sbMarkdown.AppendLine("Values from remaining args: $($this.FromRemaining)")
      $sbMarkdown.AppendLine("Do not show: $($this.DontShow)")
    }
    $sbMarkdown.AppendLine('```')
    $sbMarkdown.AppendLine()
    return $sbMarkdown.ToString()
  }
}

class DontShowAttributeInfo {
    [string] $Cmdlet
    [string] $Parameter
    [string] $ParameterType
    [bool]   $DontShow
    [string] $ParameterSetName
    [string] $Module

    [string] ToString () {
        return @(
            "$($this.Module)/"
            "$($this.Cmdlet), "
            "Parameter: $($this.Parameter) <"
            "$($this.ParameterType)>, "
            "DontShow: $($this.DontShow)"
        ) -join ''
    }
}

class ExperimentalAttributeInfo {
    [string]$Cmdlet
    [string]$Parameter
    [string]$ParameterType
    [string]$ExperimentName
    [string]$ParameterSetName
    [string]$Module

    [string] ToString () {
        return @(
            "$($this.Module)/"
            "$($this.Cmdlet), "
            "Parameter: $($this.Parameter) <"
            "$($this.ParameterType)>, "
            "Experiment: $($this.ExperimentName)"
        ) -join ''
    }
}

class HasValidationAttributeInfo {
    [string]$Cmdlet
    [string]$Parameter
    [string]$ParameterType
    [string]$ValidationAttribute
    [string]$ParameterSetName
    [string]$Module

    [string] ToString () {
        return @(
            "$($this.Module)/"
            "$($this.Cmdlet), "
            "Parameter: $($this.Parameter) <"
            "$($this.ParameterType)>, "
            "ValidationAttribute: $($this.ValidationAttribute)"
        ) -join ''
    }
}

class SupportsWildcardsAttributeInfo {
    [string]$Cmdlet
    [string]$Parameter
    [string]$ParameterType
    [bool]$SupportsWildcards
    [string]$ParameterSetName
    [string]$Module

    [string] ToString () {
        return @(
            "$($this.Module)/"
            "$($this.Cmdlet), "
            "Parameter: $($this.Parameter) <"
            "$($this.ParameterType)>"
            "SupportsWildcards: $($this.SupportsWildcards)"
        ) -join ''
    }
}

class ValueFromPipelineAttributeInfo {
    [string]$Cmdlet
    [string]$Parameter
    [string]$ParameterType
    [string]$ValueFromPipeline
    [string]$ParameterSetName
    [string]$Module

    [string] ToString () {
        return @(
            "$($this.Module)/"
            "$($this.Cmdlet), "
            "Parameter: $($this.Parameter) <"
            "$($this.ParameterType)>, "
            "ValueFromPipeline: $($this.ValueFromPipeline)"
        ) -join ''
    }
}

class ValueFromRemainingAttributeInfo {
    [string]$Cmdlet
    [string]$Parameter
    [string]$ParameterType
    [bool]$ValueFromRemaining
    [string]$ParameterSetName
    [string]$Module

    [string] ToString () {
        return @(
            "$($this.Module)/"
            "$($this.Cmdlet), "
            "Parameter: $($this.Parameter) <"
            "$($this.ParameterType)>, "
            "ValueFromRemaining: $($this.ValueFromRemaining)"
        ) -join ''
    }
}

#endregion Classes.Public

#region Functions.Private

<#
.SYNOPSIS
Returns a list of parameter headers from a cmdlet markdown file.

.DESCRIPTION
Returns a list of parameter headers from a cmdlet markdown file.

.PARAMETER mdheaders
An array of objects returned by `Select-String -Pattern '^#' -Path $file`

.NOTES
Used by `Update-ParameterOrder` to sort the parameters in a cmdlet markdown file.
#>

function Get-ParameterMdHeaders {
    param($mdheaders)

    $paramlist = @()

    $inParams = $false
    foreach ($hdr in $mdheaders) {
        # Find the start of the parameters section
        if ($hdr.Line -eq '## Parameters') {
            $inParams = $true
        }
        if ($inParams) {
            # Find the start of each parameter
            if ($hdr.Line -match '^### -') {
                $param = [PSCustomObject]@{
                    Line      = $hdr.Line.Trim()
                    StartLine = $hdr.LineNumber - 1
                    EndLine   = -1
                }
                $paramlist += $param
            }
            # Find the end of the last parameter
            if ((($hdr.Line -match '^## ' -and $hdr.Line -ne '## Parameters') -or
                 ($hdr.Line -eq '### CommonParameters')) -and
                ($paramlist.Count -gt 0)) {
                $inParams = $false
                $paramlist[-1].EndLine = $hdr.LineNumber - 2
            }
        }
    }
    # Find the end each last parameter
    if ($paramlist.Count -gt 0) {
        for ($x = 0; $x -lt $paramlist.Count; $x++) {
            if ($paramlist[$x].EndLine -eq -1) {
                $paramlist[$x].EndLine = $paramlist[($x + 1)].StartLine - 1
            }
        }
    }
    $paramlist
}

#endregion Functions.Private

#region Functions.Public

function Find-ParameterWithAttribute {
    [CmdletBinding()]
    [OutputType(
        [DontShowAttributeInfo],
        [ExperimentalAttributeInfo],
        [HasValidationAttributeInfo],
        [SupportsWildcardsAttributeInfo],
        [ValueFromPipelineAttributeInfo],
        [ValueFromRemainingAttributeInfo]
    )]
    param(
        [Parameter(Mandatory, Position = 0)]
        [ParameterAttributeKind]$AttributeKind,

        [Parameter(Position = 1)]
        [SupportsWildcards()]
        [string[]]$CommandName = '*',

        [ValidateSet('Cmdlet', 'Module', 'None')]
        [string]$GroupBy = 'None'
    )
    begin {
        $cmdlets = Get-Command $CommandName -Type Cmdlet, ExternalScript, Filter, Function, Script
    }
    process {
        foreach ($cmd in $cmdlets) {
            foreach ($param in $cmd.Parameters.Values) {
                $result = $null
                foreach ($attr in $param.Attributes) {
                    if ($attr.TypeId.ToString() -eq 'System.Management.Automation.ParameterAttribute' -and
                        $AttributeKind -in 'DontShow', 'Experimental', 'ValueFromPipeline', 'ValueFromRemaining') {
                        switch ($AttributeKind) {
                            DontShow {
                                if ($attr.DontShow) {
                                    $result = [DontShowAttributeInfo]@{
                                        Cmdlet           = $cmd.Name
                                        Parameter        = $param.Name
                                        ParameterType    = $param.ParameterType.Name
                                        DontShow         = $attr.DontShow
                                        ParameterSetName = $param.ParameterSets.Keys -join ', '
                                        Module           = $cmd.Source
                                    }
                                }
                                break
                            }
                            Experimental {
                                if ($attr.ExperimentName) {
                                    $result = [ExperimentalAttributeInfo]@{
                                        Cmdlet           = $cmd.Name
                                        Parameter        = $param.Name
                                        ParameterType    = $param.ParameterType.Name
                                        DontShow         = $attr.ExperimentName
                                        ParameterSetName = $param.ParameterSets.Keys -join ', '
                                        Module           = $cmd.Source
                                    }
                                }
                                break
                            }
                            ValueFromPipeline {
                                if ($attr.ValueFromPipeline -or $attr.ValueFromPipelineByPropertyName) {
                                    $result = [ValueFromPipelineAttributeInfo]@{
                                        Cmdlet            = $cmd.Name
                                        Parameter         = $param.Name
                                        ParameterType     = $param.ParameterType.Name
                                        ValueFromPipeline = ('ByValue({0}), ByName({1})' -f $attr.ValueFromPipeline, $attr.ValueFromPipelineByPropertyName)
                                        ParameterSetName  = $param.ParameterSets.Keys -join ', '
                                        Module            = $cmd.Source
                                    }
                                }
                                break
                            }
                            ValueFromRemaining {
                                if ($attr.ValueFromRemainingArguments) {
                                    $result = [ValueFromRemainingAttributeInfo]@{
                                        Cmdlet             = $cmd.Name
                                        Parameter          = $param.Name
                                        ParameterType      = $param.ParameterType.Name
                                        ValueFromRemaining = $attr.ValueFromRemainingArguments
                                        ParameterSetName   = $param.ParameterSets.Keys -join ', '
                                        Module             = $cmd.Source
                                    }
                                }
                                break
                            }
                        }
                    } elseif ($attr.TypeId.ToString() -like 'System.Management.Automation.Validate*Attribute' -and
                        $AttributeKind -eq 'HasValidation') {
                        $result = [HasValidationAttributeInfo]@{
                            Cmdlet              = $cmd.Name
                            Parameter           = $param.Name
                            ParameterType       = $param.ParameterType.Name
                            ValidationAttribute = $attr.TypeId.ToString().Split('.')[ - 1].Replace('Attribute', '')
                            ParameterSetName    = $param.ParameterSets.Keys -join ', '
                            Module              = $cmd.Source
                        }
                    } elseif ($attr.TypeId.ToString() -eq 'System.Management.Automation.SupportsWildcardsAttribute' -and
                        $AttributeKind -eq 'SupportsWildcards') {
                        $result = [SupportsWildcardsAttributeInfo]@{
                            Cmdlet            = $cmd.Name
                            Parameter         = $param.Name
                            ParameterType     = $param.ParameterType.Name
                            SupportsWildcards = $true
                            ParameterSetName  = $param.ParameterSets.Keys -join ', '
                            Module            = $cmd.Source
                        }
                    }
                }
                if ($result) {
                    # Add a type name to the object so that the correct format gets chosen
                    switch ($GroupBy) {
                        'Cmdlet' {
                            $typename = $result.GetType().Name + '#ByCmdlet'
                            $result.psobject.TypeNames.Insert(0, $typename)
                            break
                        }
                        'Module' {
                            $typename = $result.GetType().Name + '#ByModule'
                            $result.psobject.TypeNames.Insert(0, $typename)
                            break
                        }
                    }
                    $result
                }
            }
        }
    }
}

function Get-ParameterInfo {
    [CmdletBinding(DefaultParameterSetName = 'AsMarkdown')]
    [OutputType([ParameterInfo])]
    [OutputType([System.String])]
    param(
        [Parameter(Mandatory, Position = 0, ParameterSetName = 'AsMarkdown')]
        [Parameter(Mandatory, Position = 0, ParameterSetName = 'AsObject')]
        [string[]]$ParameterName,

        [Parameter(Mandatory, Position = 1, ParameterSetName = 'AsMarkdown')]
        [Parameter(Mandatory, Position = 1, ParameterSetName = 'AsObject')]
        [string]$CmdletName,

        [Parameter(ParameterSetName = 'AsMarkdown')]
        [switch]$ShowAll,

        [Parameter(Mandatory, ParameterSetName = 'AsObject')]
        [switch]$AsObject
    )

    $cmdlet = Get-Command -Name $CmdletName -ErrorAction Stop
    $providerList = Get-PSProvider

    foreach ($pname in $ParameterName) {
        try {
            $paraminfo = $null
            $param = $null
            foreach ($provider in $providerList) {
                Push-Location $($provider.Drives[0].Name + ':')
                $param = $cmdlet.Parameters.Values | Where-Object Name -EQ $pname
                if ($param) {
                    if ($paraminfo) {
                        $paraminfo.ProviderFlags = $paraminfo.ProviderFlags -bor [ProviderFlags]($provider.Name)
                    } else {
                        $paraminfo = [ParameterInfo]::new(
                            $param,
                            [ProviderFlags]($provider.Name)
                        )
                    }
                }
                Pop-Location
            }
        } catch {
            Write-Error "Cmdlet $CmdletName not found."
            return
        }

        if ($paraminfo) {
            if ($AsObject) {
                $paraminfo
            } else {
                $paraminfo.ToMarkdown($ShowAll)
            }
        } else {
            Write-Error "Parameter $pname not found."
        }
    }
}

function Get-ShortDescription {

    $crlf = "`r`n"
    Get-ChildItem *.md | ForEach-Object {
        if ($_.directory.basename -ne $_.basename) {
            $filename = $_.Name
            $name = $_.BaseName
            $headers = Select-String -Path $filename -Pattern '^## \w*' -AllMatches
            $mdtext = Get-Content $filename
            $start = $headers[0].LineNumber
            $end = $headers[1].LineNumber - 2
            $short = $mdtext[($start)..($end)] -join ' '
            if ($short -eq '') { $short = '{{Placeholder}}' }

            '### [{0}]({1}){3}{2}{3}' -f $name, $filename, $short.Trim(), $crlf
        }
    }

}

function Get-Syntax {

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$CmdletName,
        [switch]$Markdown
    )

    function formatString {
        param(
            $cmd,
            $pstring
        )

        $parts = $pstring -split ' '
        $parameters = @()
        for ($x = 0; $x -lt $parts.Count; $x++) {
            $p = $parts[$x]
            if ($x -lt $parts.Count - 1) {
                if (!$parts[$x + 1].StartsWith('[')) {
                    $p += ' ' + $parts[$x + 1]
                    $x++
                }
                $parameters += , $p
            } else {
                $parameters += , $p
            }
        }

        $line = $cmd + ' '
        $temp = ''
        for ($x = 0; $x -lt $parameters.Count; $x++) {
            if ($line.Length + $parameters[$x].Length + 1 -lt 100) {
                $line += $parameters[$x] + ' '
            }
            else {
                $temp += $line + "`r`n"
                $line = ' ' + $parameters[$x] + ' '
            }
        }
        $temp + $line.TrimEnd()
    }

    try {
        $cmdlet = Get-Command $cmdletname -ea Stop
        if ($cmdlet.CommandType -eq 'Alias') { $cmdlet = Get-Command $cmdlet.Definition }
        if ($cmdlet.CommandType -eq 'ExternalScript') {
            $name = $CmdletName
        } else {
            $name = $cmdlet.Name
        }

        $syntax = (Get-Command $name).ParameterSets |
            Select-Object -Property @{n = 'Cmdlet'; e = { $cmdlet.Name } },
            @{n = 'ParameterSetName'; e = { $_.name } },
            IsDefault,
            @{n = 'Parameters'; e = { $_.ToString() } }
    }
    catch [System.Management.Automation.CommandNotFoundException] {
        $_.Exception.Message
    }

    $mdHere = @'
### {0}{1}

```
{2}
```

'@


    if ($Markdown) {
        foreach ($s in $syntax) {
            $string = $s.Cmdlet, $s.Parameters -join ' '
            if ($s.IsDefault) { $default = ' (Default)' } else { $default = '' }
            if ($string.Length -gt 100) {
                $string = formatString $s.Cmdlet $s.Parameters
            }
            $mdHere -f $s.ParameterSetName, $default, $string
        }
    }
    else {
        $syntax
    }

}

function Invoke-NewMDHelp {

    ### Runs New-MarkdownHelp with the parameters we use most often.

    param(
        [Parameter(Mandatory)]
        [string]$Module,

        [Parameter(Mandatory)]
        [string]$OutPath
    )
    $parameters = @{
        Module                = $Module
        OutputFolder          = $OutPath
        AlphabeticParamsOrder = $true
        UseFullTypeName       = $true
        WithModulePage        = $true
        ExcludeDontShow       = $false
        Encoding              = [System.Text.Encoding]::UTF8
    }
    New-MarkdownHelp @parameters

}

function Invoke-Pandoc {

    param(
        [Parameter(Mandatory)]
        [string[]]$Path,
        [string]$OutputPath = '.',
        [switch]$Recurse
    )
    $pandocExe = 'C:\Program Files\Pandoc\pandoc.exe'
    Get-ChildItem $Path -Recurse:$Recurse | ForEach-Object {
        $outfile = Join-Path $OutputPath "$($_.BaseName).help.txt"
        $pandocArgs = @(
            '--from=gfm',
            '--to=plain+multiline_tables',
            '--columns=79',
            "--output=$outfile",
            '--quiet'
        )
        Get-ContentWithoutHeader $_ | & $pandocExe $pandocArgs
        Get-ChildItem $outfile
    }
}

function Update-Headings {

    param(
        [Parameter(Mandatory)]
        [SupportsWildcards()]
        [string]$Path,
        [switch]$Recurse
    )
    $headings = '## Synopsis', '## Syntax', '## Description', '## Examples', '## Parameters',
                '### CommonParameters', '## Inputs', '## Outputs', '## Notes', '## Related links',
                '## Short description', '## Long description', '## See also'

    Get-ChildItem $Path -Recurse:$Recurse | ForEach-Object {
        $_.name
        $md = Get-Content -Encoding utf8 -Path $_
        foreach ($h in $headings) {
            $md = $md -replace "^$h$", $h
        }
        Set-Content -Encoding utf8 -Value $md -Path $_ -Force
    }

}

function Update-ParameterOrder {

    [CmdletBinding()]
    [OutputType([System.IO.FileInfo])]
    param (
        [Parameter(Mandatory, Position = 0)]
        [SupportsWildcards()]
        [string[]]$Path
    )

    $mdfiles = Get-ChildItem $path

    foreach ($file in $mdfiles) {
        $mdtext = Get-Content $file -Encoding utf8
        $mdheaders = Select-String -Pattern '^#' -Path $file

        $unsorted = Get-ParameterMdHeaders $mdheaders
        if ($unsorted.Count -gt 0) {
            $sorted = $unsorted | Sort-Object Line
            $newtext = $mdtext[0..($unsorted[0].StartLine - 1)]
            $confirmWhatIf = @()
            foreach ($paramblock in $sorted) {
                if ( '### -Confirm', '### -WhatIf' -notcontains $paramblock.Line) {
                    $newtext += $mdtext[$paramblock.StartLine..$paramblock.EndLine]
                } else {
                    $confirmWhatIf += $paramblock
                }
            }
            foreach ($paramblock in $confirmWhatIf) {
                $newtext += $mdtext[$paramblock.StartLine..$paramblock.EndLine]
            }
            $newtext += $mdtext[($unsorted[-1].EndLine + 1)..($mdtext.Count - 1)]

            Set-Content -Value $newtext -Path $file.FullName -Encoding utf8 -Force
            $file
        }
    }

}

#endregion Functions.Public

$ExportableFunctions = @(
  'Find-ParameterWithAttribute'
  'Get-ParameterInfo'
  'Get-ShortDescription'
  'Get-Syntax'
  'Invoke-NewMDHelp'
  'Invoke-Pandoc'
  'Update-Headings'
  'Update-ParameterOrder'
)

Export-ModuleMember -Alias * -Function $ExportableFunctions