| 1 |
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, Position = 0)]
[string]$Path,
[Parameter(Mandatory = $false)]
[string]$OutFile,
[Parameter(Mandatory = $false)]
[string]$Title,
[Parameter(Mandatory = $false)]
[bool]$LineNumbers = $true
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Escape-Html([string]$Text) {
if ($null -eq $Text) { return '' }
return ($Text `
-replace '&', '&' `
-replace '<', '<' `
-replace '>', '>' `
-replace '"', '"')
}
function Get-TokenCssClass {
param([System.Management.Automation.PSTokenType]$Type)
switch ($Type) {
([System.Management.Automation.PSTokenType]::Attribute) { 'tok-attribute' }
([System.Management.Automation.PSTokenType]::Command) { 'tok-command' }
([System.Management.Automation.PSTokenType]::CommandArgument) { 'tok-argument' }
([System.Management.Automation.PSTokenType]::CommandParameter) { 'tok-parameter' }
([System.Management.Automation.PSTokenType]::Comment) { 'tok-comment' }
([System.Management.Automation.PSTokenType]::GroupEnd) { 'tok-punct' }
([System.Management.Automation.PSTokenType]::GroupStart) { 'tok-punct' }
([System.Management.Automation.PSTokenType]::Keyword) { 'tok-keyword' }
([System.Management.Automation.PSTokenType]::LineContinuation) { 'tok-punct' }
([System.Management.Automation.PSTokenType]::LoopLabel) { 'tok-label' }
([System.Management.Automation.PSTokenType]::Member) { 'tok-member' }
([System.Management.Automation.PSTokenType]::NewLine) { 'tok-newline' }
([System.Management.Automation.PSTokenType]::Number) { 'tok-number' }
([System.Management.Automation.PSTokenType]::Operator) { 'tok-operator' }
([System.Management.Automation.PSTokenType]::StatementSeparator){ 'tok-punct' }
([System.Management.Automation.PSTokenType]::String) { 'tok-string' }
([System.Management.Automation.PSTokenType]::Type) { 'tok-type' }
([System.Management.Automation.PSTokenType]::Variable) { 'tok-variable' }
default { 'tok-plain' }
}
}
$resolved = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
if (-not (Test-Path -LiteralPath $resolved)) {
throw "Source not found: $resolved"
}
$srcInfo = Get-Item -LiteralPath $resolved
$content = Get-Content -LiteralPath $resolved -Raw -Encoding UTF8
if ($null -eq $content) { $content = '' }
if ([string]::IsNullOrWhiteSpace($OutFile)) {
$OutFile = [System.IO.Path]::ChangeExtension($srcInfo.FullName, '.html')
}
else {
$OutFile = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutFile)
}
if ([string]::IsNullOrWhiteSpace($Title)) {
$Title = $srcInfo.Name
}
$parseErrors = $null
$tokens = [System.Management.Automation.PSParser]::Tokenize($content, [ref]$parseErrors)
$sb = [System.Text.StringBuilder]::new(($content.Length * 2) + 4096)
$pos = 0
$line = 1
foreach ($tok in $tokens) {
$start = $tok.Start
$len = $tok.Length
if ($start -lt $pos) { continue }
if ($start -gt $pos) {
$gap = $content.Substring($pos, $start - $pos)
[void]$sb.Append((Escape-Html $gap))
}
$raw = $content.Substring($start, $len)
$cls = Get-TokenCssClass -Type $tok.Type
if ($tok.Type -eq [System.Management.Automation.PSTokenType]::NewLine) {
[void]$sb.Append("`n")
}
else {
$parts = $raw -split "`n", -1
for ($pi = 0; $pi -lt $parts.Count; $pi++) {
if ($pi -gt 0) { [void]$sb.Append("`n") }
$piece = $parts[$pi]
if ($piece.Length -eq 0) { continue }
[void]$sb.Append('<span class="')
[void]$sb.Append($cls)
[void]$sb.Append('">')
[void]$sb.Append((Escape-Html $piece))
[void]$sb.Append('</span>')
}
}
$pos = $start + $len
}
if ($pos -lt $content.Length) {
[void]$sb.Append((Escape-Html ($content.Substring($pos))))
}
$codeHtml = $sb.ToString()
$bodyInner = ''
if ($LineNumbers) {
$lines = $codeHtml -split "`n", -1
$rowSb = [System.Text.StringBuilder]::new($codeHtml.Length + ($lines.Count * 48))
[void]$rowSb.Append('<table class="code-table" role="presentation"><tbody>')
for ($i = 0; $i -lt $lines.Count; $i++) {
$n = $i + 1
[void]$rowSb.Append('<tr>')
[void]$rowSb.Append('<td class="ln" data-line="')
[void]$rowSb.Append($n)
[void]$rowSb.Append('">')
[void]$rowSb.Append($n)
[void]$rowSb.Append('</td>')
[void]$rowSb.Append('<td class="code"><pre class="line">')
[void]$rowSb.Append($lines[$i])
[void]$rowSb.Append('</pre></td></tr>')
}
[void]$rowSb.Append('</tbody></table>')
$bodyInner = $rowSb.ToString()
}
else {
$bodyInner = '<pre class="code-block">' + $codeHtml + '</pre>'
}
$errNote = ''
if ($parseErrors -and @($parseErrors).Count -gt 0) {
$msgs = @($parseErrors | ForEach-Object { $_.Message }) -join '; '
$errNote = '<p class="warn">Tokenizer reported parse issues (highlighting may be incomplete): ' + (Escape-Html $msgs) + '</p>'
}
$gen = Get-Date -Format 'yyyy-MM-dd HH:mm'
$pageTitle = Escape-Html $Title
$srcLabel = Escape-Html $srcInfo.FullName
$html = @"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>$pageTitle</title>
<style>
/* VS Code Dark+ inspired */
:root {
--bg: #1e1e1e;
--fg: #d4d4d4;
--gutter: #858585;
--gutter-bg: #1e1e1e;
--border: #333333;
--header: #252526;
--accent: #007acc;
--warn-bg: #5a1d1d;
--warn-fg: #f48771;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
background: var(--bg);
color: var(--fg);
font-family: "Cascadia Code", "Fira Code", Consolas, "Courier New", monospace;
font-size: 14px;
line-height: 1.45;
}
header {
background: var(--header);
border-bottom: 1px solid var(--border);
padding: 0.75rem 1.25rem;
position: sticky;
top: 0;
z-index: 2;
}
header h1 {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: #ffffff;
}
header .meta {
margin: 0.25rem 0 0;
font-size: 0.75rem;
color: var(--gutter);
word-break: break-all;
}
.warn {
margin: 0;
padding: 0.5rem 1.25rem;
background: var(--warn-bg);
color: var(--warn-fg);
font-size: 0.8rem;
}
main { padding: 0.5rem 0 2rem; }
table.code-table {
border-collapse: collapse;
width: 100%;
table-layout: auto;
}
td.ln {
user-select: none;
text-align: right;
vertical-align: top;
padding: 0 12px 0 16px;
color: var(--gutter);
background: var(--gutter-bg);
border-right: 1px solid #404040;
width: 1%;
white-space: nowrap;
}
td.code {
vertical-align: top;
padding: 0 16px 0 12px;
width: 99%;
}
pre.line, pre.code-block {
margin: 0;
padding: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
white-space: pre-wrap;
word-break: break-word;
tab-size: 4;
}
pre.code-block { padding: 0.5rem 1.25rem; }
.tok-keyword { color: #569cd6; }
.tok-command { color: #dcdcaa; }
.tok-parameter { color: #9cdcfe; }
.tok-argument { color: #ce9178; }
.tok-string { color: #ce9178; }
.tok-variable { color: #9cdcfe; }
.tok-comment { color: #6a9955; font-style: italic; }
.tok-number { color: #b5cea8; }
.tok-operator { color: #d4d4d4; }
.tok-type { color: #4ec9b0; }
.tok-member { color: #9cdcfe; }
.tok-attribute { color: #9cdcfe; }
.tok-label { color: #c586c0; }
.tok-punct { color: #d4d4d4; }
.tok-plain { color: #d4d4d4; }
</style>
</head>
<body>
<header>
<h1>$pageTitle</h1>
<p class="meta">Source: $srcLabel · Generated $gen · Convert-Ps1ToHtml.ps1</p>
</header>
$errNote
<main>
$bodyInner
</main>
</body>
</html>
"@
$outDir = Split-Path -Parent $OutFile
if (-not [string]::IsNullOrWhiteSpace($outDir) -and -not (Test-Path -LiteralPath $outDir)) {
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
}
[System.IO.File]::WriteAllText($OutFile, $html, [System.Text.UTF8Encoding]::new($false))
Write-Host ("Wrote: {0}" -f $OutFile) -ForegroundColor Green
if ($parseErrors -and @($parseErrors).Count -gt 0) {
Write-Host ("Tokenizer warnings: {0}" -f @($parseErrors).Count) -ForegroundColor Yellow
}
return $OutFile
|