Private/ConvertTo-TLHtml.ps1

function ConvertTo-TLHtml {
    <#
    .SYNOPSIS
        Converts the Markdown subset used by TenantLens documentation to HTML.
    .DESCRIPTION
        Supports headings (#..####), bold, italic, inline code, links, unordered
        lists, tables, blockquotes and horizontal rules. Input is HTML-encoded
        first, so snapshot content cannot inject markup into the report.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [string]$Markdown
    )

    function ConvertTo-TLInlineHtml {
        param([string]$Text)
        $encoded = [System.Net.WebUtility]::HtmlEncode($Text)
        $encoded = [regex]::Replace($encoded, '`([^`]+)`', '<code>$1</code>')
        $encoded = [regex]::Replace($encoded, '\*\*([^*]+)\*\*', '<strong>$1</strong>')
        $encoded = [regex]::Replace($encoded, '(?<!\*)\*([^*]+)\*(?!\*)', '<em>$1</em>')
        $encoded = [regex]::Replace($encoded, '\[([^\]]+)\]\((https?://[^)\s]+)\)', '<a href="$2" target="_blank" rel="noopener">$1</a>')
        return $encoded
    }

    $html = [System.Text.StringBuilder]::new()
    $inList = $false
    $inTable = $false
    $inQuote = $false

    $closeBlocks = {
        if ($inList) { [void]$html.AppendLine('</ul>'); Set-Variable -Name inList -Value $false -Scope 1 }
        if ($inTable) { [void]$html.AppendLine('</tbody></table>'); Set-Variable -Name inTable -Value $false -Scope 1 }
        if ($inQuote) { [void]$html.AppendLine('</blockquote>'); Set-Variable -Name inQuote -Value $false -Scope 1 }
    }

    foreach ($line in ($Markdown -split "\r?\n")) {
        $trimmed = $line.TrimEnd()

        if ($trimmed -match '^\s*$') { & $closeBlocks; continue }

        if ($trimmed -match '^(#{1,4})\s+(.*)$') {
            & $closeBlocks
            $level = $Matches[1].Length
            [void]$html.AppendLine(('<h{0}>{1}</h{0}>' -f $level, (ConvertTo-TLInlineHtml -Text $Matches[2])))
            continue
        }

        if ($trimmed -match '^-{3,}$') { & $closeBlocks; [void]$html.AppendLine('<hr>'); continue }

        if ($trimmed -match '^\s*[-*]\s+(.*)$') {
            if (-not $inList) { & $closeBlocks; [void]$html.AppendLine('<ul>'); $inList = $true }
            [void]$html.AppendLine(('<li>{0}</li>' -f (ConvertTo-TLInlineHtml -Text $Matches[1])))
            continue
        }

        if ($trimmed -match '^\|.*\|$') {
            if ($trimmed -match '^\|[\s:|-]+\|$') { continue }  # separator row
            $cells = @(($trimmed.Trim('|') -split '\|') | ForEach-Object { $_.Trim() })
            if (-not $inTable) {
                & $closeBlocks
                [void]$html.AppendLine('<table><thead><tr>')
                foreach ($cell in $cells) { [void]$html.AppendLine(('<th>{0}</th>' -f (ConvertTo-TLInlineHtml -Text $cell))) }
                [void]$html.AppendLine('</tr></thead><tbody>')
                $inTable = $true
                continue
            }
            [void]$html.Append('<tr>')
            foreach ($cell in $cells) { [void]$html.Append(('<td>{0}</td>' -f (ConvertTo-TLInlineHtml -Text $cell))) }
            [void]$html.AppendLine('</tr>')
            continue
        }

        if ($trimmed -match '^>\s?(.*)$') {
            if (-not $inQuote) { & $closeBlocks; [void]$html.AppendLine('<blockquote>'); $inQuote = $true }
            [void]$html.AppendLine(('<p>{0}</p>' -f (ConvertTo-TLInlineHtml -Text $Matches[1])))
            continue
        }

        & $closeBlocks
        [void]$html.AppendLine(('<p>{0}</p>' -f (ConvertTo-TLInlineHtml -Text $trimmed)))
    }
    & $closeBlocks

    return $html.ToString()
}