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 } function ConvertTo-TLCellHtml { # Table cells: well-known state words render as status pills. param([string]$Text) switch ($Text.Trim().ToLowerInvariant()) { 'enabled' { return '<span class="state on">enabled</span>' } 'yes' { return '<span class="state on">yes</span>' } 'disabled' { return '<span class="state">disabled</span>' } 'no' { return '<span class="state">no</span>' } 'missing' { return '<span class="state warn">missing</span>' } } return (ConvertTo-TLInlineHtml -Text $Text) } $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-TLCellHtml -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() } |