Public/Show-Markdown.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. <# .SYNOPSIS Renders a Markdown file for viewing. .DESCRIPTION By default, converts Markdown to styled HTML and opens it in the default browser (returns immediately). With -Console, renders in the terminal using glow or pandoc. Accepts file paths as strings, FileInfo objects, or via pipeline. .EXAMPLE Show-Markdown ./README.md Opens README.md as styled HTML in the default browser. .EXAMPLE Show-Markdown ./debate.md -Console Renders debate.md in the terminal with glow. .EXAMPLE Get-ChildItem *.md | Show-MD -Console -Width 120 .EXAMPLE Show-MD ./report.md -Console -Style dark #> function Show-Markdown { [CmdletBinding()] [Alias('Show-MD')] param( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)] [Alias('FullName', 'PSPath')] [string[]]$Path, [Parameter()] [switch]$Console, [Parameter()] [int]$Width = 0, [Parameter()] [ValidateSet('dark', 'light', 'notty', 'auto')] [string]$Style = 'auto', [Parameter()] [switch]$Raw ) begin { Set-StrictMode -Version Latest $PandocPath = Get-Command pandoc -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source if ($Console -or $Raw) { $GlowPath = Get-Command glow -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source if (-not $GlowPath -and -not $PandocPath -and -not $Raw) { Write-Warning 'Neither glow nor pandoc found. Falling back to raw display. Install glow (brew install glow) for rich rendering.' $Raw = $true } } else { # Window mode needs pandoc for HTML conversion if (-not $PandocPath) { throw 'pandoc is required for window display. Install with: brew install pandoc' } } $script:HtmlStyle = @' <style> :root { color-scheme: light dark; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; max-width: 52em; margin: 2em auto; padding: 0 1.5em; line-height: 1.6; font-size: 15px; color: #1f2328; background: #fff; } @media (prefers-color-scheme: dark) { body { color: #e6edf3; background: #0d1117; } a { color: #58a6ff; } code, pre { background: #161b22; } blockquote { border-color: #3b434b; color: #8b949e; } hr { border-color: #30363d; } table th { background: #161b22; } table td, table th { border-color: #30363d; } } h1 { font-size: 1.8em; border-bottom: 1px solid #d1d9e0; padding-bottom: .3em; } h2 { font-size: 1.4em; border-bottom: 1px solid #d1d9e0; padding-bottom: .25em; margin-top: 1.5em; } h3 { font-size: 1.15em; margin-top: 1.3em; } code { font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.88em; padding: .15em .35em; background: #f0f3f6; border-radius: 4px; } pre { padding: 1em; overflow-x: auto; background: #f6f8fa; border-radius: 6px; line-height: 1.45; } pre code { padding: 0; background: none; } blockquote { margin: 0; padding: .5em 1em; border-left: 4px solid #d1d9e0; color: #656d76; } table { border-collapse: collapse; width: 100%; } table th, table td { border: 1px solid #d1d9e0; padding: .5em .8em; text-align: left; } table th { background: #f6f8fa; font-weight: 600; } hr { border: none; border-top: 1px solid #d1d9e0; margin: 1.5em 0; } a { color: #0969da; text-decoration: none; } a:hover { text-decoration: underline; } em { font-style: italic; } strong { font-weight: 600; } ul, ol { padding-left: 2em; } li + li { margin-top: .25em; } p.focus-metadata { font-style: italic; color: #1a4d8f; } @media (prefers-color-scheme: dark) { p.focus-metadata { color: #6daaed; } } </style> <script> document.addEventListener('DOMContentLoaded', function() { document.querySelectorAll('p').forEach(function(p) { if (p.textContent.trimStart().startsWith('Focus:') || (p.firstElementChild && p.firstElementChild.tagName === 'EM' && p.firstElementChild.textContent.trimStart().startsWith('Focus:'))) { p.classList.add('focus-metadata'); } }); }); </script> '@ } process { foreach ($Item in $Path) { $ResolvedPaths = @(Resolve-Path -Path $Item -ErrorAction SilentlyContinue) if ($ResolvedPaths.Count -eq 0) { Write-Error "File not found: $Item" continue } foreach ($Resolved in $ResolvedPaths) { $FilePath = $Resolved.Path if (-not (Test-Path $FilePath -PathType Leaf)) { Write-Error "Not a file: $FilePath" continue } if ($Console -or $Raw) { # ── Terminal mode ── if ($Raw) { Get-Content $FilePath -Raw continue } if ($GlowPath) { $GlowArgs = @($FilePath) if ($Width -gt 0) { $GlowArgs += @('-w', $Width) } if ($Style -ne 'auto') { $GlowArgs += @('-s', $Style) } try { & $GlowPath @GlowArgs if ($LASTEXITCODE -ne 0) { throw "glow exited with code $LASTEXITCODE" } } catch { Write-Warning "glow failed to render $FilePath : $_" if ($PandocPath) { Write-Warning 'Falling back to pandoc plain-text output' if ($Width -gt 0) { $Cols = $Width } else { $Cols = 80 } & $PandocPath $FilePath -t plain --wrap=auto "--columns=$Cols" } else { Write-Warning 'No fallback available — showing raw Markdown' Get-Content $FilePath -Raw } } } elseif ($PandocPath) { if ($Width -gt 0) { $Cols = $Width } else { $Cols = 80 } & $PandocPath $FilePath -t plain --wrap=auto "--columns=$Cols" } } else { # ── Window mode: convert to HTML and open in browser ── $BaseName = [System.IO.Path]::GetFileNameWithoutExtension($FilePath) $TempHtml = Join-Path ([System.IO.Path]::GetTempPath()) "$BaseName-$(Get-Random).html" $Title = $BaseName -replace '-', ' ' $StyleFile = Join-Path ([System.IO.Path]::GetTempPath()) "show-md-style-$(Get-Random).html" try { Set-Content -Path $StyleFile -Value $script:HtmlStyle -Encoding UTF8 -ErrorAction Stop } catch { Write-Error "Failed to write style temp file: $_`nCheck that $([System.IO.Path]::GetTempPath()) is writable." continue } $PandocArgs = @( $FilePath '-o', $TempHtml '--standalone' '--embed-resources' '--metadata', "title=$Title" '--include-in-header', $StyleFile ) $PandocErrors = @() & $PandocPath @PandocArgs 2>&1 | ForEach-Object { if ($_ -match 'WARNING') { Write-Verbose "$_" } elseif ($_ -is [System.Management.Automation.ErrorRecord]) { $PandocErrors += $_.ToString() } } Remove-Item $StyleFile -ErrorAction SilentlyContinue if (Test-Path $TempHtml) { # Open in default browser (returns immediately) try { if ($IsMacOS) { & open $TempHtml } elseif ($IsWindows) { & start $TempHtml } else { & xdg-open $TempHtml } } catch { Write-Warning "Could not open browser: $_`nHTML file saved at: $TempHtml — open it manually." } Write-Verbose "Opened: $TempHtml" } else { if ($PandocErrors.Count -gt 0) { $ErrDetail = $PandocErrors -join '; ' } else { $ErrDetail = 'unknown reason' } Write-Error "Failed to generate HTML for '$FilePath': $ErrDetail`nVerify the Markdown is valid: pandoc '$FilePath' -t plain | Select-Object -First 5" } } } } } } |