Alt3.Docusaurus.Powershell.psm1

#Region 'PREFIX' 0
Set-StrictMode -Version Latest
$PSDefaultParameterValues['*:ErrorAction'] = 'Stop' # full stop on first error
#EndRegion 'PREFIX'
#Region '.\Private\CreateOrCleanFolder.ps1' 0
function CreateOrCleanFolder() {
    <#
        .SYNOPSIS
            Helper function to create a folder OR remove it's contents if it already exists.
    #>

    param(
        [Parameter(Mandatory = $True)][string]$Path
    )

    GetCallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    # create the folder if it does not exist
    if (-not(Test-Path -Path $Path)) {
        Write-Verbose "=> creating folder $($Path)"
        New-Item -Path $Path -ItemType Directory -Force

        return
    }

    # otherwise remove it's contents
    Write-Verbose "=> cleaning folder $($Path)"
    Remove-Item -Path (Join-Path -Path $Path -ChildPath *.*)
}
#EndRegion '.\Private\CreateOrCleanFolder.ps1' 24
#Region '.\Private\EscapeClosingCurlyBrackets.ps1' 0
function EscapeClosingCurlyBrackets() {
    <#
        .SYNOPSIS
            Escape closing curly brackets so `}` becomes `\}` (except inside code blocks
            and inline code).
 
        .NOTES
            Required because MDX treats curly brackets as JSX expressions which would
            break the Docusaurus build. Code blocks and inline code need no escaping
            (and would render the backslashes literally).
    #>

    param(
        [Parameter(Mandatory = $True)][System.IO.FileSystemInfo]$MarkdownFile
    )

    $content = ReadFile -MarkdownFile $MarkdownFile

    $i = 0
    [bool]$codeblock = $False
    [bool]$frontmatter = $content[0] -eq '---'

    foreach($line in $content) {
        # skip the front matter, it is yaml data without MDX escaping requirements
        if ($frontmatter) {
            if ($i -gt 0 -and $line -eq '---') {
                $frontmatter = $False
            }

            $i++
            continue
        }

        if ($line -match '```' -and $codeblock -eq $False) {
            $codeblock = $True
        } elseif ($line -match '```' -and $codeblock -eq $True) {
            $codeBlock = $False
        }

        if ($codeblock -eq $False) {
            # transform the line except for inline code segments
            $segments = [regex]::Split($line, '(`[^`]*`)')

            for ($s = 0; $s -lt $segments.Count; $s++) {
                if ($segments[$s].StartsWith('`')) {
                    continue
                }

                $segments[$s] = [regex]::replace($segments[$s], '}', '\}')
            }

            $content[$i] = $segments -join ''
        }

        $i++
    }

    # replace file
    WriteFile -MarkdownFile $MarkdownFile -Content $content
}
#EndRegion '.\Private\EscapeClosingCurlyBrackets.ps1' 60
#Region '.\Private\EscapeOpeningCurlyBrackets.ps1' 0
function EscapeOpeningCurlyBrackets() {
    <#
        .SYNOPSIS
            Escape opening curly brackets so `{` becomes `\{` (except inside code blocks
            and inline code).
 
        .NOTES
            Required because MDX treats curly brackets as JSX expressions which would
            break the Docusaurus build. Code blocks and inline code need no escaping
            (and would render the backslashes literally).
    #>

    param(
        [Parameter(Mandatory = $True)][System.IO.FileSystemInfo]$MarkdownFile
    )

    $content = ReadFile -MarkdownFile $MarkdownFile

    $i = 0
    [bool]$codeblock = $False
    [bool]$frontmatter = $content[0] -eq '---'

    foreach($line in $content) {
        # skip the front matter, it is yaml data without MDX escaping requirements
        if ($frontmatter) {
            if ($i -gt 0 -and $line -eq '---') {
                $frontmatter = $False
            }

            $i++
            continue
        }

        if ($line -match '```' -and $codeblock -eq $False) {
            $codeblock = $True
        } elseif ($line -match '```' -and $codeblock -eq $True) {
            $codeBlock = $False
        }

        if ($codeblock -eq $False) {
            # transform the line except for inline code segments
            $segments = [regex]::Split($line, '(`[^`]*`)')

            for ($s = 0; $s -lt $segments.Count; $s++) {
                if ($segments[$s].StartsWith('`')) {
                    continue
                }

                $segments[$s] = [regex]::replace($segments[$s], '{', '\{')
            }

            $content[$i] = $segments -join ''
        }

        $i++
    }

    # replace file
    WriteFile -MarkdownFile $MarkdownFile -Content $content
}
#EndRegion '.\Private\EscapeOpeningCurlyBrackets.ps1' 60
#Region '.\Private\GetCallerPreference.ps1' 0
function GetCallerPreference {
    <#
    .Synopsis
       Fetches "Preference" variable values from the caller's scope.
    .DESCRIPTION
       Script module functions do not automatically inherit their caller's variables, but they can be
       obtained through the $PSCmdlet variable in Advanced Functions. This function is a helper function
       for any script module Advanced Function; by passing in the values of $ExecutionContext.SessionState
       and $PSCmdlet, GetCallerPreference will set the caller's preference variables locally.
    .PARAMETER Cmdlet
       The $PSCmdlet object from a script module Advanced Function.
    .PARAMETER SessionState
       The $ExecutionContext.SessionState object from a script module Advanced Function. This is how the
       GetCallerPreference function sets variables in its callers' scope, even if that caller is in a different
       script module.
    .PARAMETER Name
       Optional array of parameter names to retrieve from the caller's scope. Default is to retrieve all
       Preference variables as defined in the about_Preference_Variables help file (as of PowerShell 4.0)
       This parameter may also specify names of variables that are not in the about_Preference_Variables
       help file, and the function will retrieve and set those as well.
    .EXAMPLE
       GetCallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
 
       Imports the default PowerShell preference variables from the caller into the local scope.
    .EXAMPLE
       GetCallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -Name 'ErrorActionPreference','SomeOtherVariable'
 
       Imports only the ErrorActionPreference and SomeOtherVariable variables into the local scope.
    .EXAMPLE
       'ErrorActionPreference','SomeOtherVariable' | GetCallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
 
       Same as Example 2, but sends variable names to the Name parameter via pipeline input.
    .INPUTS
       String
    .OUTPUTS
       None. This function does not produce pipeline output.
    .LINK
       https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d
    #>


    [CmdletBinding(DefaultParameterSetName = 'AllVariables')]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript( { $_.GetType().FullName -eq 'System.Management.Automation.PSScriptCmdlet' })]
        $Cmdlet,

        [Parameter(Mandatory = $true)]
        [System.Management.Automation.SessionState]
        $SessionState,

        [Parameter(ParameterSetName = 'Filtered', ValueFromPipeline = $true)]
        [string[]]
        $Name
    )

    begin {
        $filterHash = @{}
    }

    process {
        if ($null -ne $Name) {
            foreach ($string in $Name) {
                $filterHash[$string] = $true
            }
        }
    }

    end {
        # List of preference variables taken from the about_Preference_Variables help file in PowerShell version 4.0

        $vars = @{
            'ErrorView'                     = $null
            'FormatEnumerationLimit'        = $null
            'LogCommandHealthEvent'         = $null
            'LogCommandLifecycleEvent'      = $null
            'LogEngineHealthEvent'          = $null
            'LogEngineLifecycleEvent'       = $null
            'LogProviderHealthEvent'        = $null
            'LogProviderLifecycleEvent'     = $null
            'MaximumAliasCount'             = $null
            'MaximumDriveCount'             = $null
            'MaximumErrorCount'             = $null
            'MaximumFunctionCount'          = $null
            'MaximumHistoryCount'           = $null
            'MaximumVariableCount'          = $null
            'OFS'                           = $null
            'OutputEncoding'                = $null
            'ProgressPreference'            = $null
            'PSDefaultParameterValues'      = $null
            'PSEmailServer'                 = $null
            'PSModuleAutoLoadingPreference' = $null
            'PSSessionApplicationName'      = $null
            'PSSessionConfigurationName'    = $null
            'PSSessionOption'               = $null

            'ErrorActionPreference'         = 'ErrorAction'
            'DebugPreference'               = 'Debug'
            'ConfirmPreference'             = 'Confirm'
            'WhatIfPreference'              = 'WhatIf'
            'VerbosePreference'             = 'Verbose'
            'WarningPreference'             = 'WarningAction'
        }


        foreach ($entry in $vars.GetEnumerator()) {
            if (([string]::IsNullOrEmpty($entry.Value) -or -not $Cmdlet.MyInvocation.BoundParameters.ContainsKey($entry.Value)) -and
                ($PSCmdlet.ParameterSetName -eq 'AllVariables' -or $filterHash.ContainsKey($entry.Name))) {
                $variable = $Cmdlet.SessionState.PSVariable.Get($entry.Key)

                if ($null -ne $variable) {
                    if ($SessionState -eq $ExecutionContext.SessionState) {
                        Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false
                    }
                    else {
                        $SessionState.PSVariable.Set($variable.Name, $variable.Value)
                    }
                }
            }
        }

        if ($PSCmdlet.ParameterSetName -eq 'Filtered') {
            foreach ($varName in $filterHash.Keys) {
                if (-not $vars.ContainsKey($varName)) {
                    $variable = $Cmdlet.SessionState.PSVariable.Get($varName)

                    if ($null -ne $variable) {
                        if ($SessionState -eq $ExecutionContext.SessionState) {
                            Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false
                        }
                        else {
                            $SessionState.PSVariable.Set($variable.Name, $variable.Value)
                        }
                    }
                }
            }
        }

    } # end

} # function GetCallerPreference
#EndRegion '.\Private\GetCallerPreference.ps1' 141
#Region '.\Private\GetCustomEditUrl.ps1' 0
function GetCustomEditUrl() {
    <#
        .SYNOPSIS
            Returns the `custom_edit_url` for the given .md file.
 
        .DESCRIPTION
            Generates a URL pointing to the PowerShell source file that was used to generate the markdown file.
 
        .NOTES
            - passing string `null` will return string `null`
            - URLs for non-monolithic modules point to a .ps1 file with same name as the markdown file
            - URLs for monolithic modules will always point to a .psm1 with same name as passed module
    #>

    param(
        [Parameter(Mandatory = $True)][string]$Module,
        [Parameter(Mandatory = $True)][System.IO.FileSystemInfo]$MarkdownFile,
        [Parameter(Mandatory = $False)][string]$EditUrl,
        [switch]$Monolithic
    )

    # return $null so Docusaurus will not render the `Edit this page` button
    if (-not $EditUrl) {
        return
    }

    # if string "null" was passed explicitely, return as-is
    if ($EditUrl -eq "null") {
        return "null"
    }

    # removing trailing slashes
    $EditUrl = $EditUrl.TrimEnd("/")

    # point to the function source file for non-monlithic modules
    if (-not $Monolithic) {
        $command = [System.IO.Path]::GetFileNameWithoutExtension($MarkdownFile)

        return $EditUrl + '/' + $command + ".ps1"
    }

    # point to the module source file for monolithic modules
    if (Test-Path $Module) {
        $Module = [System.IO.Path]::GetFileNameWithoutExtension($Module)
    }

    return $EditUrl + '/' + $Module + ".psm1"
}
#EndRegion '.\Private\GetCustomEditUrl.ps1' 48
#Region '.\Private\HtmlEncodeGreaterThanBrackets.ps1' 0
function HtmlEncodeGreaterThanBrackets() {
    <#
        .SYNOPSIS
            Html encode `>` brackets, both raw and backslash-escaped (except inside code
            blocks and inline code).
 
        .NOTES
            Ensures brackets render as-authored because MDX/markdown would otherwise
            swallow the backslash (or render a blockquote).
    #>

    param(
        [Parameter(Mandatory = $True)][System.IO.FileSystemInfo]$MarkdownFile
    )

    $content = ReadFile -MarkdownFile $MarkdownFile

    $i = 0
    [bool]$codeblock = $False
    [bool]$frontmatter = $content[0] -eq '---'

    foreach($line in $content) {
        # skip the front matter, it is yaml data without MDX escaping requirements
        if ($frontmatter) {
            if ($i -gt 0 -and $line -eq '---') {
                $frontmatter = $False
            }

            $i++
            continue
        }

        if ($line -match '```' -and $codeblock -eq $False) {
            $codeblock = $True
        } elseif ($line -match '```' -and $codeblock -eq $True) {
            $codeBlock = $False
        }

        if ($codeblock -eq $False) {
            # transform the line except for inline code segments
            $segments = [regex]::Split($line, '(`[^`]*`)')

            for ($s = 0; $s -lt $segments.Count; $s++) {
                if ($segments[$s].StartsWith('`')) {
                    continue
                }

                $segments[$s] = [regex]::replace($segments[$s], '([a-zA-Z]:)\\\>', '$1\\&gt;') # something special for C:\>
                $segments[$s] = [regex]::replace($segments[$s], '\\\>', '&gt;') # backslash-escaped brackets
                $segments[$s] = [regex]::replace($segments[$s], '\>', '&gt;') # raw brackets
            }

            $content[$i] = $segments -join ''
        }

        $i++
    }

    # replace file
    WriteFile -MarkdownFile $MarkdownFile -Content $content
}
#EndRegion '.\Private\HtmlEncodeGreaterThanBrackets.ps1' 61
#Region '.\Private\HtmlEncodeLessThanBrackets.ps1' 0
function HtmlEncodeLessThanBrackets() {
    <#
        .SYNOPSIS
            Html encode `<` brackets, both raw and backslash-escaped (except inside code
            blocks and inline code).
 
        .NOTES
            Required because MDX treats raw `<` brackets as JSX component tags which
            would break the Docusaurus build.
    #>

    param(
        [Parameter(Mandatory = $True)][System.IO.FileSystemInfo]$MarkdownFile
    )

    $content = ReadFile -MarkdownFile $MarkdownFile

    $i = 0
    [bool]$codeblock = $False
    [bool]$frontmatter = $content[0] -eq '---'

    foreach($line in $content) {
        # skip the front matter, it is yaml data without MDX escaping requirements
        if ($frontmatter) {
            if ($i -gt 0 -and $line -eq '---') {
                $frontmatter = $False
            }

            $i++
            continue
        }

        if ($line -match '```' -and $codeblock -eq $False) {
            $codeblock = $True
        } elseif ($line -match '```' -and $codeblock -eq $True) {
            $codeBlock = $False
        }

        if ($codeblock -eq $False) {
            # transform the line except for inline code segments
            $segments = [regex]::Split($line, '(`[^`]*`)')

            for ($s = 0; $s -lt $segments.Count; $s++) {
                if ($segments[$s].StartsWith('`')) {
                    continue
                }

                $segments[$s] = [regex]::replace($segments[$s], '(\\\\\\\<|\\\<)', '&lt;') # backslash-escaped brackets
                $segments[$s] = [regex]::replace($segments[$s], '\<', '&lt;') # raw brackets
            }

            $content[$i] = $segments -join ''
        }

        $i++
    }

    # replace file
    WriteFile -MarkdownFile $MarkdownFile -Content $content
}
#EndRegion '.\Private\HtmlEncodeLessThanBrackets.ps1' 60
#Region '.\Private\IndentLineBelowOpeningBracket.ps1' 0
function IndentLineBelowOpeningBracket() {
    <#
        .SYNOPSIS
            Indent line directly below line with opening curly bracket.
 
        .NOTES
            Because PlatyPS sometimes gets the indentation wrong with complex examples.
 
        .LINK
            https://regex101.com/r/eMCf3E/1
    #>

    param(
        [Parameter(Mandatory = $True)][System.IO.FileSystemInfo]$MarkdownFile
    )

    GetCallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    Write-Verbose "Removing blank lines above closing curly bracket"

    $content = ReadFile -MarkdownFile $MarkdownFile -Raw

    $regex = [regex]::new('({\n)([^\s+].+)')

    $content = $content -replace $regex, "`$1 `$2"

    # replace file
    WriteFile -MarkdownFile $MarkdownFile -Content $content
}
#EndRegion '.\Private\IndentLineBelowOpeningBracket.ps1' 29
#Region '.\Private\IndentLineWithOpeningBracket.ps1' 0
function IndentLineWithOpeningBracket() {
    <#
        .SYNOPSIS
            Corrects indentation for lines with opening curly brackets and incorrect indentation
            by comparing indentation of the line below (and recalculating if things are amiss).
 
        .NOTES
            Skips correcting if the line below has 4-space indentation
 
        .NOTES
            The regex gives us three useful matching groups:
            - Group 1 is the full first without the line feed
            - Group 2 is the full second line without the line feed
            - Group 3 contains the leading spaces of the second line
 
        .LINK
            https://regex101.com/r/WYGbfX/1
    #>

    param(
        [Parameter(Mandatory = $True)][System.IO.FileSystemInfo]$MarkdownFile
    )

    GetCallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    Write-Verbose "Removing blank lines above closing curly bracket"

    $content = ReadFile -MarkdownFile $MarkdownFile -Raw

    $regex = [regex]::new('(?m)^([^\s].+{)\n((\s+)(.+))')

    $callback = {
        param($match)

        # do nothing if next line starts with 4 spaces
        if ($match.Groups[3].Value.Length -eq 4) {
            return $match
        }

        # divide spacing of next line by 2 and use that as correct indentation
        [string]$fixedIndentation = ""
        $fixedIndentation.PadRight(($match.Groups[3].Value.Length / 2 - 1), " ")

        $fixedIndentation + $match.Groups[1] + "`n" + $match.Groups[2]
    }

    $content = $regex.replace($content, $callback)

    # replace file
    WriteFile -MarkdownFile $MarkdownFile -Content $content
}
#EndRegion '.\Private\IndentLineWithOpeningBracket.ps1' 51
#Region '.\Private\InitializeTempFolder.ps1' 0
function InitializeTempFolder() {
    <#
        .SYNOPSIS
            Creates the temp folder and the `debug.info` file.
 
        .DESCRIPTION
            The temp folder is where all work is done before the enriched mdx files are copied
            to the docusaurus sidebar folder. We use this approach to support future debugging
            as it will be near impossible to reason about bugs without looking at the PlatyPS
            generated source files, knowing which PowerShell version was used etc.
 
        .NOTES
            Ideally, we should also log used module versions for Alt3, PlatyPS and Pester.
    #>

    param(
        [Parameter(Mandatory = $True)][string]$Path
    )

    GetCallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    # create the folder
    Write-Verbose "Initializing temp folder:"
    CreateOrCleanFolder -Path $Path

    # log the module parameters used for this run
    $ParameterList = (Get-Command -Name New-DocusaurusHelp).Parameters
    $parameterHash = [ordered]@{ }

    $ParameterList.Keys | ForEach-Object {
        $variable = (Get-Variable -Name $_ -ErrorAction SilentlyContinue)

        if ($null -eq $variable) { # Verbose, ErrorAction, etc.
            return
        }

        if ($_ -eq 'CommandHelp' -and $null -ne $variable.Value) { # log command names instead of the deep CommandHelp objects
            $parameterHash.Add($_, @($variable.Value | ForEach-Object { $_.Title }))
            return
        }

        $parameterHash.Add($_, $variable.Value)
    }

    # create the hash with debug information
    $debugInfo = [ordered]@{
        ModuleParameters = $parameterHash
        PSVersionTable   = $PSVersionTable
    } | ConvertTo-Json -Depth 5

    # create the debug file
    Write-Verbose "=> preparing debug file"
    $debugFile = Join-Path -Path $Path -ChildPath "debug.json"
    $fileEncoding = New-Object System.Text.UTF8Encoding $False

    [System.IO.File]::WriteAllLines($debugFile, $debugInfo, $fileEncoding)
}
#EndRegion '.\Private\InitializeTempFolder.ps1' 57
#Region '.\Private\InsertFinalNewline.ps1' 0
function InsertFinalNewline() {
    <#
        .SYNOPSIS
            Adds a traling newline to the end of the file.
    #>

    param(
        [Parameter(Mandatory = $True)][System.IO.FileSystemInfo]$MarkdownFile
    )

    $content = ReadFile -MarkdownFile $MarkdownFile -Raw

    # replace file
    WriteFile -MarkdownFile $MarkdownFile -Content ($content + "`n")
}
#EndRegion '.\Private\InsertFinalNewline.ps1' 15
#Region '.\Private\InsertPowerShellMonikers.ps1' 0
function InsertPowerShellMonikers() {
    <#
        .SYNOPSIS
            Adds the `powershell` moniker to the code blocks in the SYNTAX section.
 
        .NOTES
            We need to do this because PlatyPS does not add the moniker to the syntax
            code blocks itself. Only the SYNTAX section is processed because all other
            (user authored) code blocks should be left untouched.
    #>

    param(
        [Parameter(Mandatory = $True)][System.IO.FileSystemInfo]$MarkdownFile
    )

    $content = ReadFile -MarkdownFile $MarkdownFile -Raw

    $regexSyntaxSection = [regex]'(?s)## SYNTAX.*?(?=\n## )'

    $content = $regexSyntaxSection.Replace($content, {
        param($match)

        $regexBareFencedBlock = [regex]'(```)\n((?:(?!```)[\s\S])+)(```)'
        $regexBareFencedBlock.Replace($match.Value, '```powershell' + "`n" + '$2```')
    }, 1)

    # replace file
    WriteFile -MarkdownFile $MarkdownFile -Content $content
}
#EndRegion '.\Private\InsertPowerShellMonikers.ps1' 29
#Region '.\Private\InsertUserMarkdown.ps1' 0
function InsertUserMarkdown() {
    <#
        .SYNOPSIS
            Inserts user provided markdown directly above OR below the PlatyPS generated markdown.
 
        .NOTES
            Will use file content as markdown if $Markdown resolves to a file.
    #>

    param(
        [Parameter(Mandatory = $True)][System.IO.FileSystemInfo]$MarkdownFile,
        [Parameter(Mandatory = $False)][string]$Markdown,
        [Parameter(Mandatory = $True)][ValidateSet('Prepend', 'Append')][string]$Mode
    )

    GetCallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $content = ReadFile -MarkdownFile $MarkdownFile -Raw

    # use file content as markdown
    if ( $(try { Test-Path $Markdown.Trim() } catch { $false }) ) {
        $Markdown = Get-Content -Path $Markdown -Raw
    }

    # remove any leading or trailing newlines
    $Markdown = $Markdown.TrimStart()
    $Markdown = $Markdown.TrimEnd()

    # convert CRLF to LF
    $Markdown = $Markdown -replace "`r`n", "`n"

    # insert user markdown
    if ($Mode -eq "Prepend") {
        Write-Verbose "=> prepending user markdown"

        $regex = '(---\n\n)'
        $content = $content -replace $regex, "---`n`n$Markdown`n`n"
    }
    else {
        Write-Verbose "=> appending user markdown"

        $content = "$content`n`n$Markdown`n`n"
    }

    # create new file
    WriteFile -MarkdownFile $MarkdownFile -Content $content
}
#EndRegion '.\Private\InsertUserMarkdown.ps1' 47
#Region '.\Private\NewMarkdownExample.ps1' 0
function NewMarkdownExample() {
    <#
        .SYNOPSIS
            Generates a new markdown example block.
    #>

    param(
        [Parameter(Mandatory = $True)][string]$Header,
        [Parameter(Mandatory = $True)][string]$Code,
        [Parameter(Mandatory = $False)][string]$Description = $null
    )

    $example = "$Header`n`n"
    $example += '```powershell' + "`n"
    $example += "$($Code.Trim([char]10))`n"
    $example += '```'

    if (-not [string]::IsNullOrWhiteSpace($Description)) {
        $example += "`n`n$($Description.Trim([char]10))"
    }

    return $example
}
#EndRegion '.\Private\NewMarkdownExample.ps1' 23
#Region '.\Private\NewSidebarIncludeFile.ps1' 0
function NewSidebarIncludeFile() {
    <#
        .SYNOPSIS
            Generates a `.js` file holding an array with all .mdx 'ids` to be imported in Docusaurus `sidebar.js`.
 
        .LINK
            https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-powershell-1.0/ff730948(v=technet.10)
    #>

    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "Sidebar",
        Justification = 'False positive as rule does not scan child scopes')]
    param(
        [Parameter(Mandatory = $True)][string]$TempFolder,
        [Parameter(Mandatory = $True)][string]$OutputFolder,
        [Parameter(Mandatory = $True)][string]$Sidebar,
        [Parameter(Mandatory = $True)][Object]$MarkdownFiles,
        [Parameter(Mandatory = $True)][Version]$Alt3Version
    )

    GetCallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    Write-Verbose "Generating docusaurus.sidebar.js"

    # generate a list of PowerShell commands by stripping .md from the generated PlatyPs files
    [array]$commands = $MarkdownFiles | Select-Object @{ Name = "PowerShellCommand"; Expression = { "'$Sidebar/" + [System.IO.Path]::GetFileNameWithoutExtension($_) + "'" } } | Select-Object  -Expand PowerShellCommand

    # generate content using Here-String block
    $content = @"
/**
 * Import this file in your Docusaurus ``sidebars.js`` file.
 *
 * Auto-generated by Alt3.Docusaurus.Powershell $($Alt3Version).
 *
 * Copyright (c) 2019-present, ALT3 B.V.
 *
 * Licensed under the MIT license.
 */
 
module.exports = [
    $($commands -Join ",`n ")
];
"@


    # create the temp file
    $fileName = "docusaurus.sidebar.js"
    $tempFile = Join-Path -Path $tempFolder -ChildPath $fileName
    $fileEncoding = New-Object System.Text.UTF8Encoding $False
    [System.IO.File]::WriteAllLines($tempFile, $content, $fileEncoding)

    # copy to the sidebar folder, convert relative output folder to absolute if needed
    if (-Not([System.IO.Path]::IsPathRooted($OutputFolder))) {
        $outputFolder = Join-Path "$(Get-Location)" -ChildPath $OutputFolder
    }

    Copy-Item -Path $tempFile -Destination (Join-Path -Path $outputFolder -ChildPath $fileName)
}
#EndRegion '.\Private\NewSidebarIncludeFile.ps1' 56
#Region '.\Private\ReadFile.ps1' 0
function ReadFile() {
    <#
        .SYNOPSIS
            Retrieves raw markdown from file.
    #>

    param(
        [Parameter(Mandatory = $True)][System.IO.FileSystemInfo]$MarkdownFile,
        [switch]$Raw
    )

    # file content as string
    if ($Raw) {
        return (Get-Content -Path $MarkdownFile.FullName -Raw).TrimEnd()
    }

    # file content as array of lines
    Get-Content -Path $MarkdownFile.FullName
}
#EndRegion '.\Private\ReadFile.ps1' 19
#Region '.\Private\RemoveAliasesSection.ps1' 0
function RemoveAliasesSection() {
    <#
        .SYNOPSIS
            Removes the PlatyPS generated ALIASES section when it only contains the
            `{{Insert list of aliases}}` placeholder.
 
        .NOTES
            ALIASES sections with real content (e.g. imported from user-enriched
            markdown or CommandHelp objects) are left untouched.
    #>

    param(
        [Parameter(Mandatory = $True)][System.IO.FileSystemInfo]$MarkdownFile
    )

    $content = ReadFile -MarkdownFile $MarkdownFile -Raw

    $regexAliasesSection = [regex]'\n## ALIASES\n[\s\S]*?(?=\n## )'

    $content = $regexAliasesSection.Replace($content, {
        param($match)

        if ($match.Value -match '{{Insert list of aliases}}') {
            return ''
        }

        return $match.Value
    }, 1)

    # replace file
    WriteFile -MarkdownFile $MarkdownFile -Content $content
}
#EndRegion '.\Private\RemoveAliasesSection.ps1' 32
#Region '.\Private\RemoveBlankLinesAboveClosingBracket.ps1' 0
function RemoveBlankLinesAboveClosingBracket() {
    <#
        .SYNOPSIS
            Removes blank lines ABOVE lines ending with a closing curly bracket.
 
        .NOTES
            Required so following steps can trust formatting.
 
        .LINK
            https://regex101.com/r/jMBHcT/1
    #>

    param(
        [Parameter(Mandatory = $True)][System.IO.FileSystemInfo]$MarkdownFile
    )

    GetCallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    Write-Verbose "Removing blank lines above closing curly bracket"

    $content = ReadFile -MarkdownFile $MarkdownFile -Raw

    $regex = [regex]::new('(\n\n+\s+}|\n\n})')

    $content = $content -replace $regex, "`n}"

    # replace file
    WriteFile -MarkdownFile $MarkdownFile -Content $content
}
#EndRegion '.\Private\RemoveBlankLinesAboveClosingBracket.ps1' 29
#Region '.\Private\RemoveBlankLinesBelowOpeningBracket.ps1' 0
function RemoveBlankLinesBelowOpeningBracket() {
    <#
        .SYNOPSIS
            Removes blank lines below lines ending with an opening curly bracket.
 
        .NOTES
            Required so following steps can trust formatting.
 
        .LINK
            https://regex101.com/r/FAdpGh/1
    #>

    param(
        [Parameter(Mandatory = $True)][System.IO.FileSystemInfo]$MarkdownFile
    )

    GetCallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    Write-Verbose "Removing blank lines below opening curly bracket"

    $content = ReadFile -MarkdownFile $MarkdownFile -Raw

    $regex = [regex]::new('({\n+\n)')

    $content = $content -replace $regex, "{`n"

    # replace file
    WriteFile -MarkdownFile $MarkdownFile -Content $content
}
#EndRegion '.\Private\RemoveBlankLinesBelowOpeningBracket.ps1' 29
#Region '.\Private\RemoveDefaultParameterSetHeading.ps1' 0
function RemoveDefaultParameterSetHeading() {
    <#
        .SYNOPSIS
            Removes the `### __AllParameterSets` heading PlatyPS generates in the SYNTAX
            section for commands without named parameter sets.
 
        .NOTES
            Headings for named parameter sets are left untouched.
    #>

    param(
        [Parameter(Mandatory = $True)][System.IO.FileSystemInfo]$MarkdownFile
    )

    $content = ReadFile -MarkdownFile $MarkdownFile -Raw

    $content = $content -replace '### __AllParameterSets\n\n', ''

    # replace file
    WriteFile -MarkdownFile $MarkdownFile -Content $content
}
#EndRegion '.\Private\RemoveDefaultParameterSetHeading.ps1' 21
#Region '.\Private\RemoveRedundantBlankLines.ps1' 0
function RemoveRedundantBlankLines() {
    <#
        .SYNOPSIS
            Collapses consecutive blank lines (outside code blocks) into a single blank line.
 
        .NOTES
            Blank lines inside code blocks are left untouched because they are part of the
            user authored example code.
    #>

    param(
        [Parameter(Mandatory = $True)][System.IO.FileSystemInfo]$MarkdownFile
    )

    $content = ReadFile -MarkdownFile $MarkdownFile

    $newContent = [System.Collections.Generic.List[string]]::new()
    [bool]$codeblock = $False
    [bool]$previousLineBlank = $False
    [bool]$frontmatter = $content[0] -eq '---'
    $i = 0

    foreach($line in $content) {
        # skip the front matter, blank lines inside multi-line yaml values are data
        if ($frontmatter) {
            if ($i -gt 0 -and $line -eq '---') {
                $frontmatter = $False
            }

            $i++
            $newContent.Add($line)
            continue
        }
        $i++

        if ($line -match '```') {
            $codeblock = -not $codeblock
        }

        $lineBlank = [string]::IsNullOrWhiteSpace($line)

        if (-not $codeblock -and $lineBlank -and $previousLineBlank) {
            continue
        }

        $previousLineBlank = $lineBlank
        $newContent.Add($line)
    }

    # replace file
    WriteFile -MarkdownFile $MarkdownFile -Content $newContent
}
#EndRegion '.\Private\RemoveRedundantBlankLines.ps1' 52
#Region '.\Private\RemoveSectionPlaceholders.ps1' 0
function RemoveSectionPlaceholders() {
    <#
        .SYNOPSIS
            Removes PlatyPS generated placeholders from the INPUTS, OUTPUTS, NOTES and
            RELATED LINKS sections, leaving the (required) section headings empty instead.
 
        .NOTES
            The SYNOPSIS and DESCRIPTION placeholders are kept because they remind
            module authors to complete their Get-Help definitions.
 
        .NOTES
            The INPUTS and OUTPUTS placeholders are removed because PlatyPS also
            generates them for comment-based help that DOES describe the type (Get-Help
            returns the entire .INPUTS/.OUTPUTS text as the type name, leaving the
            description PlatyPS looks at empty).
    #>

    param(
        [Parameter(Mandatory = $True)][System.IO.FileSystemInfo]$MarkdownFile
    )

    $content = ReadFile -MarkdownFile $MarkdownFile -Raw

    $content = $content -replace '\n\{\{ Fill in the (Notes|related links here) \}\}\n?', ''

    # remove the type description placeholders in the INPUTS and OUTPUTS sections only
    $regexInputsOutputsSections = [regex]'(?s)## INPUTS.*?(?=\n## NOTES)'

    $content = $regexInputsOutputsSections.Replace($content, {
        param($match)

        return $match.Value -replace '\n\{\{ Fill in the Description \}\}\n?', ''
    }, 1)

    # replace file
    WriteFile -MarkdownFile $MarkdownFile -Content $content
}
#EndRegion '.\Private\RemoveSectionPlaceholders.ps1' 37
#Region '.\Private\RepairRelatedLinks.ps1' 0
function RepairRelatedLinks() {
    <#
        .SYNOPSIS
            Repairs PlatyPS generated RELATED LINKS so they render correctly.
 
        .DESCRIPTION
            PlatyPS renders `.LINK` help entries as markdown list items but:
 
            - bare urls produce a link without clickable text => `- [](url)`
            - text entries produce a link without a target => `- [text]()`
 
            This function uses the url as the link text for the first case and
            a plain text list item for the second case.
    #>

    param(
        [Parameter(Mandatory = $True)][System.IO.FileSystemInfo]$MarkdownFile
    )

    $content = ReadFile -MarkdownFile $MarkdownFile -Raw

    $regexRelatedLinksSection = [regex]'(?s)## RELATED LINKS.*$'

    $content = $regexRelatedLinksSection.Replace($content, {
        param($match)

        $section = [regex]::Replace($match.Value, '(?m)^- \[\]\((.+?)\)', '- [$1]($1)')
        $section = [regex]::Replace($section, '(?m)^- \[(.+?)\]\(\)', '- $1')

        return $section
    }, 1)

    # replace file
    WriteFile -MarkdownFile $MarkdownFile -Content $content
}
#EndRegion '.\Private\RepairRelatedLinks.ps1' 35
#Region '.\Private\ReplaceExamples.ps1' 0
function ReplaceExamples() {
    <#
        .SYNOPSIS
            Replaces PlatyPS generated example sections with Docusaurus compatible markdown examples.
 
        .DESCRIPTION
            PlatyPS preserves the authored example help almost verbatim which means we need to:
 
            - wrap "native" examples (not using a code fence) in a powershell fenced code block
            - normalize the language moniker of fenced examples (```, ```ps and ```posh all become ```powershell)
            - replace the PlatyPS placeholder example (generated for commands without Get-Help
              definitions) with a Docusaurus friendly variant
            - insert a placeholder example when the command has no examples at all
 
            The `-NoPlaceholderExamples` switch drops placeholder examples instead, resulting
            in an empty `EXAMPLES` section.
    #>

    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "NoPlaceHolderExamples",
        Justification = 'False positive as rule does not scan child scopes')]
    param(
        [Parameter(Mandatory = $True)][System.IO.FileSystemInfo]$MarkdownFile,
        [switch]$NoPlaceHolderExamples
    )

    GetCallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $content = ReadFile -MarkdownFile $MarkdownFile -Raw
    $newExamples = [System.Collections.Generic.List[string]]::new()

    # ---------------------------------------------------------------------
    # extract all EXAMPLE nodes
    # ---------------------------------------------------------------------
    $regexExtractExamples = [regex]'### (EXAMPLE|Example) [0-9]+[\s\S]*?(?=\n### (EXAMPLE|Example) [0-9]+|\n## PARAMETERS|$)'
    $examples = $regexExtractExamples.Matches($content)

    # process each EXAMPLE node
    $examples | ForEach-Object {
        $example = $_.Value.Trim([char]10)

        # split the markdown header from the example body
        $header, $body = $example -split "`n", 2
        $header = $header.Trim()
        $body = "$body".Trim([char]10)

        # ---------------------------------------------------------------------
        # PlatyPS placeholder example, generated for commands without a Get-Help
        # definition => replace with Docusaurus friendly variant (or drop)
        # ---------------------------------------------------------------------
        if ($body -match '{{ Add example description here }}' -and $body -notmatch '```') {
            if ($NoPlaceHolderExamples) {
                Write-Verbose "=> $($header): PlatyPS Placeholder (dropping)"
                return
            }

            Write-Verbose "=> $($header): PlatyPS Placeholder (keeping)"
            $newExamples.Add((NewMarkdownExample -Header "### Example 1" -Code 'PS C:\> {{ Add example code here }}' -Description '{{ Add example description here }}'))
            return
        }

        # ---------------------------------------------------------------------
        # code fenced example => normalize the moniker of the leading code fence
        # (all other code fences are left untouched)
        # ---------------------------------------------------------------------
        $regexLeadingFence = [regex]'^```(ps|posh|powershell)?[ \t]*\n'

        if ($body -match $regexLeadingFence) {
            Write-Verbose "=> $($header): Code Fenced example"

            $body = $regexLeadingFence.Replace($body, '```powershell' + "`n", 1)
            $newExamples.Add("$header`n`n$body")
            return
        }

        # ---------------------------------------------------------------------
        # native example (no code fence) => Get-Help treats the first paragraph
        # as code so we wrap it in a powershell fenced code block
        # ---------------------------------------------------------------------
        Write-Verbose "=> $($header): Native example"

        $code, $description = $body -split "`n`r?`n", 2
        $newExamples.Add((NewMarkdownExample -Header $header -Code $code -Description "$description"))
    }

    # ---------------------------------------------------------------------
    # no examples at all => insert a placeholder example (or leave empty)
    # ---------------------------------------------------------------------
    if ($examples.Count -eq 0 -and -not $NoPlaceHolderExamples) {
        Write-Verbose "=> No examples found: inserting placeholder example"
        $newExamples.Add((NewMarkdownExample -Header "### Example 1" -Code 'PS C:\> {{ Add example code here }}' -Description '{{ Add example description here }}'))
    }

    # replace EXAMPLES section in content with updated examples
    $regex = '## EXAMPLES\n[\s\S]+?## PARAMETERS'
    $joinedExamples = ($newExamples -join "`n`n").Replace('$', '$$') # Escape regex replacement $-substitutions (https://github.com/alt3/Docusaurus.Powershell/pull/98)

    if ($newExamples.Count -eq 0) {
        $replacement = "## EXAMPLES`n`n## PARAMETERS"
    } else {
        $replacement = "## EXAMPLES`n`n$($joinedExamples)`n`n## PARAMETERS"
    }

    $content = [regex]::replace($content, $regex, $replacement)

    # replace file
    WriteFile -MarkdownFile $MarkdownFile -Content $content
}
#EndRegion '.\Private\ReplaceExamples.ps1' 107
#Region '.\Private\ReplaceFrontMatter.ps1' 0
function ReplaceFrontMatter() {
    <#
        .SYNOPSIS
            Replaces PlatyPS generated front matter with Docusaurus compatible front matter.
 
        .DESCRIPTION
            The PlatyPS-native front matter keys are removed and replaced with the
            Docusaurus front matter variables this module generates.
 
            All other keys are preserved as-is which allows users to enrich the front
            matter themselves (e.g. by adding a `description` key to `CommandHelp.Metadata`
            before passing the objects to `New-DocusaurusHelp -CommandHelp`). Preserved
            keys always win over the values this module would generate, so e.g. an
            existing `description` key is not overwritten by `-MetaDescription`.
 
        .LINK
            https://github.com/alt3/Docusaurus.Powershell/issues/185
    #>

    param(
        [Parameter(Mandatory = $True)][System.IO.FileSystemInfo]$MarkdownFile,
        [Parameter(Mandatory = $False)][string]$CustomEditUrl,
        [Parameter(Mandatory = $False)][string]$MetaDescription,
        [Parameter(Mandatory = $False)][array]$MetaKeywords,
        [switch]$HideTitle,
        [switch]$HideTableOfContents
    )

    $powershellCommandName = [System.IO.Path]::GetFileNameWithoutExtension($markdownFile.Name)

    $content = ReadFile -MarkdownFile $MarkdownFile -Raw

    # ---------------------------------------------------------------------
    # parse the existing front matter into (multi-line) entries, keyed by
    # their top-level yaml key
    # ---------------------------------------------------------------------
    $regexFrontMatter = [regex]"(?sm)^(---)(.+?)^(---).$\n"
    $existingEntries = [ordered]@{ }

    $match = $regexFrontMatter.Match($content)
    if ($match.Success) {
        $currentKey = $null

        foreach ($line in ($match.Groups[2].Value.Trim([char]10) -split "`n")) {
            if ($line -match '^(?<key>[A-Za-z][A-Za-z0-9_. -]*):') {
                $currentKey = $Matches.key
                $existingEntries[$currentKey] = $line
            } elseif ($null -ne $currentKey -and $line -match '^\s+\S') {
                $existingEntries[$currentKey] += "`n$line" # multi-line value (e.g. a list)
            }
        }
    }

    # remove the PlatyPS-native keys (all other keys are user-enriched and thus preserved)
    @('document type', 'external help file', 'HelpUri', 'Locale', 'Module Name', 'ms.date', 'PlatyPS schema version') | ForEach-Object {
        $existingEntries.Remove($_)
    }

    # also remove the PlatyPS-native title (but preserve user-customized titles)
    if ("$($existingEntries['title'])" -match "^title:\s*$([regex]::Escape($powershellCommandName))\s*$") {
        $existingEntries.Remove('title')
    }

    # ---------------------------------------------------------------------
    # prepare new front matter, preserved keys win over generated values
    # ---------------------------------------------------------------------
    $newFrontMatter = [System.Collections.ArrayList]::new()
    $newFrontMatter.Add("---") | Out-Null

    $addEntry = {
        param($key, $generatedEntry)

        if ($existingEntries.Contains($key)) {
            $newFrontMatter.Add($existingEntries[$key]) | Out-Null
            $existingEntries.Remove($key)
        } elseif ($null -ne $generatedEntry) {
            $newFrontMatter.Add($generatedEntry) | Out-Null
        }
    }

    & $addEntry 'id' "id: $($powershellCommandName)"
    & $addEntry 'title' "title: $($powershellCommandName)"

    $description = $null
    if ($MetaDescription) {
        $description = "description: $([regex]::replace($MetaDescription, '%1', $powershellCommandName))"
    }
    & $addEntry 'description' $description

    $keywords = $null
    if ($MetaKeywords) {
        $keywords = "keywords:`n" + (($MetaKeywords | ForEach-Object { " - $($_)" }) -join "`n")
    }
    & $addEntry 'keywords' $keywords

    & $addEntry 'hide_title' "hide_title: $(if ($HideTitle) {"true"} else {"false"})"
    & $addEntry 'hide_table_of_contents' "hide_table_of_contents: $(if ($HideTableOfContents) {"true"} else {"false"})"

    $editUrl = $null
    if ($CustomEditUrl) {
        $editUrl = "custom_edit_url: $($CustomEditUrl)"
    }
    & $addEntry 'custom_edit_url' $editUrl

    # append remaining user-enriched keys
    foreach ($entry in $existingEntries.Values) {
        $newFrontMatter.Add($entry) | Out-Null
    }

    $newFrontMatter.Add("---") | Out-Null

    # translate front matter to a string and replace CRLF with LF
    $newFrontMatter = ($newFrontMatter | Out-String) -replace "`r`n", "`n"

    # replace front matter
    $content = $regexFrontMatter.Replace($content, $newFrontMatter, 1)

    # replace file
    WriteFile -MarkdownFile $MarkdownFile -Content $content
}
#EndRegion '.\Private\ReplaceFrontMatter.ps1' 120
#Region '.\Private\ReplaceHeader1.ps1' 0
function ReplaceHeader1() {
    <#
        .SYNOPSIS
            Removes the markdown H1 element OR preprends it with an extra newline if the -KeepHeader1 switch is used.
 
        .LINK
            https://regex101.com/r/hnVQvQ/1
    #>

    param(
        [Parameter(Mandatory = $True)][System.IO.FileSystemInfo]$MarkdownFile,
        [switch]$KeepHeader1
    )

    $content = ReadFile -MarkdownFile $MarkdownFile -Raw

    $regex = '(---)(\n\n|\n)(# .+)'

    if ($KeepHeader1) {
        $content = $content -replace $regex, ("---`n`n" + '$3') # prepend newline (for first match only)
    } else {
        $content = $content -replace $regex, '---' # remove line (for first match only)
    }

    # replace file
    WriteFile -MarkdownFile $MarkdownFile -Content $content
}
#EndRegion '.\Private\ReplaceHeader1.ps1' 27
#Region '.\Private\SeparateMarkdownHeadings.ps1' 0
function SeparateMarkdownHeadings() {
    <#
        .SYNOPSIS
            Adds a blank line after markdown headers IF they are directly followed by an adjacent non-blank lines.
 
        .NOTES
            This ensures the markdown format will match with e.g. Prettier which in turn will
            prevent getting format-change suggestions when running e.g. > Visual Studio Code
            > CTRL+SHIFT+P > Format Document.
 
        .LINK
            https://regex101.com/r/llYF0H/1
    #>

    param(
        [Parameter(Mandatory = $True)][System.IO.FileSystemInfo]$MarkdownFile
    )

    GetCallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    Write-Verbose "Inserting blank line beneath non-separated headers."

    $content = ReadFile -MarkdownFile $MarkdownFile -Raw

    $regex = [regex]::new('(?m)^\n^([#]#{0,5}[a-z]*\s.+)\n(.+)')

    $content = $content -replace $regex, "`n`$1`n`n`$2"

    # replace file
    WriteFile -MarkdownFile $MarkdownFile -Content $content
}
#EndRegion '.\Private\SeparateMarkdownHeadings.ps1' 31
#Region '.\Private\SetLfLineEndings.ps1' 0
function SetLfLineEndings() {
    <#
        .SYNOPSIS
            Replaces all CRLF line endings with LF so we can consitently use/expect `n when regexing etc.
    #>

    param(
        [Parameter(Mandatory = $True)][System.IO.FileSystemInfo]$MarkdownFile
    )

    $content = ReadFile -MarkdownFile $MarkdownFile -Raw

    $content = ($content -replace "`r`n", "`n") + "`n"

    WriteFile -MarkdownFile $MarkdownFile -Content $content
}
#EndRegion '.\Private\SetLfLineEndings.ps1' 16
#Region '.\Private\WriteFile.ps1' 0
function WriteFile() {
    <#
        .SYNOPSIS
            Writes content to a UTF-8 file without BOM using LF as newlines.
    #>

    param(
        [Parameter(Mandatory = $True)][System.IO.FileSystemInfo]$MarkdownFile,
        [Parameter(Mandatory = $True)]$Content
    )

    # replace file (UTF-8 without BOM)
    $fileEncoding = New-Object System.Text.UTF8Encoding $False

    # when content is a string
    if (($Content.GetType().Name -eq "String")) {
        [System.IO.File]::WriteAllText($MarkdownFile.FullName, $Content, $fileEncoding)

        return
    }

    # when content is an array
    [System.IO.File]::WriteAllLines($MarkdownFile.FullName, $Content, $fileEncoding)
}
#EndRegion '.\Private\WriteFile.ps1' 24
#Region '.\Public\New-DocusaurusHelp.ps1' 0
function New-DocusaurusHelp() {
    <#
        .SYNOPSIS
            Generates Get-Help documentation in Docusaurus compatible `.mdx` format.
 
        .DESCRIPTION
            The `New-DocusaurusHelp` cmdlet generates Get-Help documentation in "Docusaurus
            compatible" format by creating an `.mdx` file for each command exported by
            the module, enriched with command-specific front matter variables.
 
            Also creates a `sidebar.js` file for simplified integration into the Docusaurus sidebar menu.
 
            **Supports two input modes**, matching the SYNTAX sections shown above:
 
            - `Module`: generates documentation for all commands exported by the given module
            - `CommandHelp`: generates documentation for the given PlatyPS `CommandHelp` objects, allowing you to pre-process them first
 
        .OUTPUTS
            System.IO.FileInfo
 
            One file object for each generated file so the results are ready for further processing.
 
        .EXAMPLE
            New-DocusaurusHelp -Module Alt3.Docusaurus.Powershell
 
            This example uses default settings to generate a Get-Help page for each command exported by
            the Alt3.Docusaurus.Powershell module.
 
        .EXAMPLE
            ```
            $parameters = @{
                Module = "Alt3.Docusaurus.Powershell"
                DocsFolder = "D:\my-project\docs"
                Sidebar = "commands"
                Exclude = @(
                    "Get-SomeCommand"
                )
                MetaDescription = 'Help page for the PowerShell command "%1"'
                MetaKeywords = @(
                    "PowerShell"
                    "Documentation"
                )
            }
 
            New-DocusaurusHelp @parameters
            ```
 
            This example uses
            [splatting](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_splatting)
            to override default settings.
 
            See the list of Parameters below for all available overrides.
 
        .PARAMETER Module
            Specifies the module this cmdlet will generate Docusaurus documentation for.
 
            You may specify a module name, a `.psd1` file or a `.psm1` file.
 
        .PARAMETER CommandHelp
            Specifies one or more `Microsoft.PowerShell.PlatyPS.Model.CommandHelp` objects, as produced
            by the PlatyPS cmdlets `New-CommandHelp`, `Import-MarkdownCommandHelp` or `Import-YamlCommandHelp`.
 
            Use this parameter if you want to pre-process the help objects before this module
            transforms them into Docusaurus pages, e.g.:
 
            ```
            $commandHelp = New-CommandHelp -CommandInfo (Get-Command -Module MyModule)
            $commandHelp[0].Synopsis = "An updated synopsis"
            New-DocusaurusHelp -CommandHelp $commandHelp
            ```
 
            Keys added to the `Metadata` property (e.g. `description`) will appear in the
            Docusaurus front matter and always win over the generated variables.
 
            Also use this parameter if you have already prepared PlatyPS markdown files,
            e.g. `New-DocusaurusHelp -CommandHelp (Import-MarkdownCommandHelp -Path $files)`.
 
        .PARAMETER DocsFolder
            Specifies the absolute or relative **path** to the Docusaurus `docs` folder.
 
            Optional, defaults to `docusaurus/docs`, case sensitive.
 
        .PARAMETER Sidebar
            Specifies the **name** of the docs subfolder in which the `.mdx` files will be created.
 
            Optional, defaults to `commands`, case sensitive.
 
        .PARAMETER Exclude
            Optional array with command names to exclude.
 
        .PARAMETER MetaDescription
            Optional string that will be inserted into Docusaurus front matter to be used as html meta tag 'description'.
 
            If placeholder `%1` is detected in the string, it will be replaced by the command name.
 
            Will not overwrite an existing `description` front matter key (e.g. added
            via the CommandHelp `Metadata` property).
 
        .PARAMETER MetaKeywords
            Optional array of keywords inserted into Docusaurus front matter to be used as html meta tag `keywords`.
 
        .PARAMETER PrependMarkdown
            Optional string containing raw markdown **OR** path to a markdown file.
 
            Markdown will be inserted in all pages, directly above the PlatyPS generated markdown.
 
        .PARAMETER AppendMarkdown
            Optional string containing raw markdown **OR** path to a markdown file.
 
            Markdown will be inserted in all pages, directly below the PlatyPS generated markdown.
 
        .PARAMETER EditUrl
            Specifies the URL prefixed to all Docusaurus `custom_edit_url` front matter variables.
 
            Optional, defaults to `null`.
 
        .PARAMETER KeepHeader1
            By default, the `H1` element will be removed from the PlatyPS generated markdown because
            Docusaurus uses the per-page frontmatter variable `title` as the page's H1 element instead.
 
            You may use this switch parameter to keep the markdown `H1` element, most likely in
            combination with the `HideTitle` parameter.
 
        .PARAMETER HideTitle
            Sets the Docusaurus front matter variable `hide_title`.
 
            Optional, defaults to `false`.
 
        .PARAMETER HideTableOfContents
            Sets the Docusaurus front matter variable `hide_table_of_contents`.
 
            Optional, defaults to `false`.
 
        .PARAMETER NoPlaceholderExamples
            By default, Docusaurus will generate a placeholder example if your Get-Help
            definition does not contain any `EXAMPLE` nodes.
 
            You can use this switch to disable that behavior which will result in an empty `EXAMPLES` section.
 
        .PARAMETER Monolithic
            Use this optional parameter if the PowerShell module source is monolithic.
 
            Will point all `custom_edit_url` front matter variables to the `.psm1` file.
 
        .PARAMETER VendorAgnostic
            Use this switch parameter if you **do not want to use Docusaurus** but would still like
            to benefit of the markdown-enrichment functions this module provides.
 
            If used, the `New-GetDocusaurusHelp` command will produce the exact same markdown as
            always but will skip the following two Docusaurus-specific steps:
 
            - PlatyPS frontmatter will not be touched
            - `docusaurus.sidebar.js` file will not be generated
 
            For more information please
            [visit this page](https://docusaurus-powershell.vercel.app/docs/faq/vendor-agnostic).
 
        .NOTES
            For debugging purposes, Docusaurus.Powershell creates a local temp folder with:
 
            - the raw PlatyPS generated `.md` files
            - the Docusaurus.Powershell enriched `.mdx` files
            - a `debug.json` file containing detailed module information
 
            ```powershell
            $tempFolder = Get-Item (Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath "Alt3.Docusaurus.Powershell")
            ```
 
        .LINK
            https://docusaurus-powershell.vercel.app/
 
        .LINK
            https://docusaurus.io/
 
        .LINK
            https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.platyps/
    #>

    [cmdletbinding()]
    param(
        [Parameter(Mandatory = $True, ParameterSetName = 'Module')][string]$Module,
        [Parameter(Mandatory = $True, ParameterSetName = 'CommandHelp')]
        [ValidateScript({
            if ($_.GetType().FullName -ne 'Microsoft.PowerShell.PlatyPS.Model.CommandHelp') {
                throw "Expected a Microsoft.PowerShell.PlatyPS.Model.CommandHelp object but received '$($_.GetType().FullName)'. Use e.g. New-CommandHelp or Import-MarkdownCommandHelp to create CommandHelp objects."
            }
            $true
        })]
        [object[]]$CommandHelp,
        [Parameter(Mandatory = $False)][string]$DocsFolder = "docusaurus/docs",
        [Parameter(Mandatory = $False)][string]$Sidebar = "commands",
        [Parameter(Mandatory = $False)][array]$Exclude = @(),
        [Parameter(Mandatory = $False)][string]$EditUrl,
        [Parameter(Mandatory = $False)][string]$MetaDescription,
        [Parameter(Mandatory = $False)][array]$MetaKeywords,
        [Parameter(Mandatory = $False)][string]$PrependMarkdown,
        [Parameter(Mandatory = $False)][string]$AppendMarkdown,
        [switch]$KeepHeader1,
        [switch]$HideTitle,
        [switch]$HideTableOfContents,
        [switch]$NoPlaceHolderExamples,
        [switch]$Monolithic,
        [switch]$VendorAgnostic
    )

    GetCallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    # normalize all parameter sets into PlatyPS CommandHelp objects
    switch ($PSCmdlet.ParameterSetName) {
        'Module' {
            # make sure the passed module is valid
            if (Test-Path($Module)) {
                Import-Module $Module -Force -Global
                $Module = [System.IO.Path]::GetFileNameWithoutExtension($Module)
            }

            if (-Not(Get-Module -Name $Module)) {
                throw "New-DocusaurusHelp: Specified module '$Module' is not loaded"
            }

            Write-Verbose "Generating PlatyPS CommandHelp objects."
            # use ExportedCommands because Get-Command -Module would also return private
            # functions when a module (like this one) generates its own documentation
            $moduleCommands = (Get-Module -Name $Module).ExportedCommands.Values |
                Where-Object { $_.CommandType -in 'Cmdlet', 'Function', 'Filter' }

            $commandHelpObjects = foreach ($moduleCommand in $moduleCommands) {
                try {
                    # child scope disables StrictMode which breaks PlatyPS (https://github.com/PowerShell/platyPS/issues/800)
                    $newCommandHelp = & {
                        Set-StrictMode -Off
                        New-CommandHelp -CommandInfo $moduleCommand -ErrorAction Stop
                    }

                    # restore the synopsis discarded by PlatyPS for commands with a Get-Help
                    # definition without .DESCRIPTION and .EXAMPLE nodes
                    if ($newCommandHelp.Synopsis -eq '{{ Fill in the Synopsis }}') {
                        $helpSynopsis = (Get-Help -Name $moduleCommand.Name).Synopsis

                        if ($helpSynopsis -notmatch "^\s*$([regex]::Escape($moduleCommand.Name))") {
                            $newCommandHelp.Synopsis = $helpSynopsis.Trim()
                        }
                    }

                    $newCommandHelp
                } catch {
                    Write-Warning "Unable to generate help for command '$($moduleCommand.Name)': $($_.Exception.Message)"
                    Write-Warning "Known PlatyPS limitation: commands using an .EXAMPLE without a .DESCRIPTION in their comment-based help will fail."
                }
            }
        }
        'CommandHelp' {
            $commandHelpObjects = $CommandHelp
        }
    }

    if (-not $commandHelpObjects) {
        throw "New-DocusaurusHelp: no command help could be generated, unable to continue"
    }

    # remove excluded commands
    if ($Exclude.Count -gt 0) {
        $commandHelpObjects = @($commandHelpObjects | Where-Object { $Exclude -notcontains $_.Title })
    }

    # determine the module name
    if ($PSCmdlet.ParameterSetName.Equals('Module'))
    {
        $moduleName = [io.path]::GetFileName($module)
    }
    else
    {
        # get the module's name from the supplied CommandHelp objects
        $moduleName = @($commandHelpObjects.ModuleName | Select-Object -Unique)

        # Throw if null or we've got more than one item.
        if ($moduleName.Count -eq 0 -or [string]::IsNullOrEmpty($moduleName[0]))
        {
            $errSentence1 = 'Unable to determine the module name from the supplied command help.'
            $errSentence2 = 'Please confirm its validity and try again.'
            $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new(
                    [System.ArgumentException]::new("$errSentence1 $errSentence2", $PSCmdlet.ParameterSetName),
                    'ModuleNameIndeterminateError',
                    [System.Management.Automation.ErrorCategory]::InvalidResult,
                    $moduleName
                ))
        }
        elseif ($moduleName.Count -gt 1)
        {
            $errSentence1 = "More than one module name was found within the supplied command help ('$([System.String]::Join("', '", $moduleName))')."
            $errSentence2 = 'Please supply command help for a single module and try again.'
            $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new(
                    [System.ArgumentException]::new("$errSentence1 $errSentence2", $PSCmdlet.ParameterSetName),
                    'DuplicateModuleNameError',
                    [System.Management.Automation.ErrorCategory]::InvalidResult,
                    $moduleName
                ))
        }

        $moduleName = $moduleName[0]
        $Module = $moduleName
    }

    # get version of this module so we can e.g. add version tag to generated files
    $alt3Version = Split-Path -Leaf $MyInvocation.MyCommand.ScriptBlock.Module.ModuleBase
    Write-Verbose "Using Alt3 module version = $($alt3Version)"

    # markdown for the module will be copied into the sidebar subfolder
    Write-Verbose "Initializing sidebar folder:"
    $sidebarFolder = Join-Path -Path $DocsFolder -ChildPath $Sidebar
    CreateOrCleanFolder -Path $sidebarFolder

    # create tempfolder used for generating the PlatyPS files and creating the mdx files
    $tempFolder = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath "Alt3.Docusaurus.Powershell" | Join-Path -ChildPath $moduleName
    InitializeTempFolder -Path $tempFolder

    # generate PlatyPS markdown files
    Write-Verbose "Generating PlatyPS markdown files."
    & {
        # child scope disables StrictMode which breaks PlatyPS (https://github.com/PowerShell/platyPS/issues/800)
        Set-StrictMode -Off
        $commandHelpObjects | Export-MarkdownCommandHelp -OutputFolder $tempFolder -Force
    } | Out-Null

    # PlatyPS exports into a module-named subfolder, flatten it into the temp folder
    $moduleSubFolder = Join-Path -Path $tempFolder -ChildPath $moduleName
    if (Test-Path -Path $moduleSubFolder) {
        Get-ChildItem -Path $moduleSubFolder -Filter *.md | Move-Item -Destination $tempFolder -Force
        Remove-Item -Path $moduleSubFolder -Recurse -Force
    }

    if (-not (Get-ChildItem -Path $tempFolder -Filter *.md)) {
        throw "New-DocusaurusHelp: PlatyPS did not generate any markdown files for module '$moduleName'"
    }

    # rename PlatyPS files and create an `.mdx` copy we will transform
    Write-Verbose "Cloning PlatyPS files."
    Get-ChildItem -Path $tempFolder -Filter *.md | ForEach-Object {
        $platyPsFile = $_.FullName -replace '\.md$', '.PlatyPS.md'
        $mdxFile = $_.FullName -replace '\.md$', '.mdx'
        Move-Item -Path $_.FullName -Destination $platyPsFile
        Copy-Item  -Path $platyPsFile -Destination $mdxFile
    }

    # update all remaining mdx files to make them Docusaurus compatible
    Write-Verbose "Updating mdx files."
    $mdxFiles = Get-ChildItem -Path $tempFolder -Filter *.mdx

    ForEach ($mdxFile in $mdxFiles) {
        Write-Verbose "Processing $($mdxFile.Name):"

        # prepare per-page variables
        $customEditUrl = GetCustomEditUrl -Module $Module -MarkdownFile $mdxFile -EditUrl $EditUrl -Monolithic:$Monolithic

        $frontMatterArgs = @{
            MarkdownFile = $mdxFile
            MetaDescription = $metaDescription
            CustomEditUrl = $customEditUrl
            MetaKeywords = $metaKeywords
            HideTitle = $HideTitle
            HideTableOfContents = $HideTableOfContents
        }

        # transform the markdown using these steps (overwriting the mdx file per step)
        SetLfLineEndings -MarkdownFile $mdxFile

        if (-not($VendorAgnostic)) {
            ReplaceFrontMatter @frontmatterArgs
        }

        ReplaceHeader1 -MarkdownFile $mdxFile -KeepHeader1:$KeepHeader1

        # remove PlatyPS generated noise
        RemoveAliasesSection -MarkdownFile $mdxFile
        RemoveDefaultParameterSetHeading -MarkdownFile $mdxFile
        RemoveSectionPlaceholders -MarkdownFile $mdxFile
        RepairRelatedLinks -MarkdownFile $mdxFile

        if ($PrependMarkdown) {
            InsertUserMarkdown -MarkdownFile $mdxFile -Markdown $PrependMarkdown -Mode "Prepend"
        }

        ReplaceExamples -MarkdownFile $mdxFile -NoPlaceholderExamples:$NoPlaceholderExamples

        if ($AppendMarkdown) {
            InsertUserMarkdown -MarkdownFile $mdxFile -Markdown $AppendMarkdown -Mode "Append"
        }

        # Post-fix complex multiline code examples (https://github.com/pester/Pester/issues/2195)
        RemoveBlankLinesBelowOpeningBracket -MarkdownFile $mdxFile
        RemoveBlankLinesAboveClosingBracket -MarkdownFile $mdxFile
        IndentLineBelowOpeningBracket -MarkdownFile $mdxFile
        IndentLineWithOpeningBracket -MarkdownFile $mdxFile

        ## Continue with general enrichment
        InsertPowerShellMonikers -MarkdownFile $mdxFile
        SeparateMarkdownHeadings -MarkdownFile $mdxFile

        # Line by line changes
        HtmlEncodeLessThanBrackets -MarkdownFile $mdxFile
        HtmlEncodeGreaterThanBrackets -MarkdownFile $mdxFile
        EscapeOpeningCurlyBrackets -MarkdownFile $mdxFile
        EscapeClosingCurlyBrackets -MarkdownFile $mdxFile
        RemoveRedundantBlankLines -MarkdownFile $mdxFile

        # all done, set line endings again
        SetLfLineEndings -MarkdownFile $mdxFile
        InsertFinalNewline -MarkdownFile $mdxFile
    }

    # copy updated mdx files to the target folder
    Write-Verbose "Copying mdx files to sidebar folder."
    Get-ChildItem -Path $tempFolder -Filter *.mdx | ForEach-Object {
        Copy-Item  -Path $_.FullName -Destination (Join-Path -Path $sidebarFolder -ChildPath ($_.Name))
    }

    # generate the `.js` file used for the docusaurus sidebar
    if (-not($VendorAgnostic)) {
        NewSidebarIncludeFile -MarkdownFiles $mdxFiles -TempFolder $tempFolder -OutputFolder $sidebarFolder -Sidebar $Sidebar -Alt3Version $alt3Version
    }

    # output Get-ChildItem so end-user can post-process generated files as they see fit
    Get-ChildItem -Path $sidebarFolder
}
#EndRegion '.\Public\New-DocusaurusHelp.ps1' 424