Public/ConvertTo-ConfluenceHTML.ps1

function ConvertTo-ConfluenceHTML {
    <#
    .SYNOPSIS
        Converts simple Markdown to HTML for a Confluence page body.

    .DESCRIPTION
        ConvertTo-ConfluenceHTML performs a lightweight, line-based Markdown conversion. Supported:
        headings (# to ######), **bold**, *italic*, bullet lists ("- " or "* "; consecutive items share
        one list), fenced code blocks (```), whose content is kept exactly and left untouched by the
        other rules, and tables: consecutive | a | b | lines become one table, a |---|---| separator
        row is skipped and marks the row above it as the header row.

        All text is escaped (& < >), so the result is well-formed storage format and HTML in the
        Markdown is shown as text. Other lines are passed through as escaped text, one per line.

        It is not a full Markdown parser; nested lists, links, images and inline code are left as
        text.

    .PARAMETER InputContent
        The Markdown text to convert.

    .PARAMETER InputFormat
        The format of the input. Only Markdown is supported.

    .EXAMPLE
        ConvertTo-ConfluenceHTML -InputFormat Markdown -InputContent "# Title`n- one`n- two"

        Returns <h1>Title</h1> followed by <ul><li>one</li><li>two</li></ul>.

    .EXAMPLE
        ConvertTo-ConfluenceHTML -InputFormat Markdown -InputContent "| Name | Count |`n|---|---|`n| a | 1 |"

        Returns one table with a header row (Name, Count) and one data row.

    .OUTPUTS
        System.String
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param (
        [Parameter(Mandatory = $true, HelpMessage = 'The input content to be converted.')]
        [AllowEmptyString()]
        [string]$InputContent,

        [Parameter(Mandatory = $true, HelpMessage = "The format of the input content. Currently supported: 'Markdown'.")]
        [ValidateSet('Markdown')]
        [string]$InputFormat
    )

    $TelemetryArgs = @{
        ModuleName    = $MyInvocation.MyCommand.Module.Name
        ModuleVersion = [string]$MyInvocation.MyCommand.Module.Version
        CommandName   = $MyInvocation.MyCommand.Name
        ExecutionID   = [guid]::NewGuid().ToString()
    }
    Invoke-TelemetryCollection @TelemetryArgs -Stage Start -ClearTimer
    $telemetryFailed = $false
    try {
        switch ($InputFormat) {
            'Markdown' {
                $inline = {
                    param([string]$Text)
                    $html = ConvertTo-ConfluenceXmlText -Text $Text
                    $html = $html -replace '\*\*(.+?)\*\*', '<strong>$1</strong>'
                    $html = $html -replace '\*(.+?)\*', '<em>$1</em>'
                    return $html
                }
                $splitRow = {
                    param([string]$Line)
                    $cells = $Line.Trim()
                    if ($cells.StartsWith('|')) { $cells = $cells.Substring(1) }
                    if ($cells.EndsWith('|')) { $cells = $cells.Substring(0, $cells.Length - 1) }
                    # "\|" is a literal pipe inside a cell
                    return , @($cells.Replace('\|', [string][char]0) -split '\|' | ForEach-Object { $_.Replace([string][char]0, '|').Trim() })
                }
                $separatorPattern = '^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$'

                $lines = ($InputContent -replace "`r`n", "`n") -split "`n"
                $blocks = New-Object -TypeName System.Collections.Generic.List[string]
                $index = 0
                while ($index -lt $lines.Count) {
                    $line = $lines[$index]
                    if ($line -match '^\s*```') {
                        # Fenced code block: everything up to the closing fence is kept as-is
                        $code = New-Object -TypeName System.Collections.Generic.List[string]
                        $index++
                        while ($index -lt $lines.Count -and $lines[$index] -notmatch '^\s*```') {
                            $code.Add($lines[$index])
                            $index++
                        }
                        $index++
                        $blocks.Add('<pre><code>' + (ConvertTo-ConfluenceXmlText -Text ($code -join "`n")) + '</code></pre>')
                        continue
                    }
                    if ($line -match '^(#{1,6})\s+(.*)$') {
                        $level = $Matches[1].Length
                        $blocks.Add("<h$level>" + (& $inline $Matches[2]) + "</h$level>")
                        $index++
                        continue
                    }
                    if ($line -match '^\s*[-*]\s+') {
                        $items = ''
                        while ($index -lt $lines.Count -and $lines[$index] -match '^\s*[-*]\s+(.*)$') {
                            $items += '<li>' + (& $inline $Matches[1]) + '</li>'
                            $index++
                        }
                        $blocks.Add("<ul>$items</ul>")
                        continue
                    }
                    if ($line -match '^\s*\|.*\|\s*$') {
                        $tableLines = New-Object -TypeName System.Collections.Generic.List[string]
                        while ($index -lt $lines.Count -and $lines[$index] -match '^\s*\|.*\|\s*$') {
                            $tableLines.Add($lines[$index])
                            $index++
                        }
                        $hasHeader = $tableLines.Count -gt 1 -and $tableLines[1] -match $separatorPattern
                        $rows = ''
                        for ($rowIndex = 0; $rowIndex -lt $tableLines.Count; $rowIndex++) {
                            if ($tableLines[$rowIndex] -match $separatorPattern) { continue }
                            $cellTag = if ($hasHeader -and $rowIndex -eq 0) { 'th' } else { 'td' }
                            $cells = foreach ($cell in (& $splitRow $tableLines[$rowIndex])) { "<$cellTag>" + (& $inline $cell) + "</$cellTag>" }
                            $rows += '<tr>' + ($cells -join '') + '</tr>'
                        }
                        $blocks.Add("<table><tbody>$rows</tbody></table>")
                        continue
                    }
                    $blocks.Add((& $inline $line))
                    $index++
                }
                $HtmlContent = $blocks -join "`n"
                return $HtmlContent
            }
        }
    }
    catch {
        $telemetryFailed = $true
        Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed $true -Exception $_
        throw
    }
    finally {
        if (-not $telemetryFailed) {
            Invoke-TelemetryCollection @TelemetryArgs -Stage End
        }
    }
}