Docs/Convert-MdToPdf.ps1
|
# Convert-MdToPdf.ps1 - dependency-free Markdown -> PDF for the SqlCertForge docs. # Pipeline: PowerShell 7 ConvertFrom-Markdown -> styled HTML -> headless Edge --print-to-pdf. # Relative image paths (e.g. images/x.svg) resolve because the temp HTML is written # beside the .md. Requires PowerShell 7 (ConvertFrom-Markdown) and Microsoft Edge. [CmdletBinding()] param( [Parameter(Mandatory)][string] $MdPath, [string] $PdfPath, [string] $Title = 'SqlCertForge' ) $md = Get-Content -LiteralPath $MdPath -Raw $bodyHtml = (ConvertFrom-Markdown -InputObject $md).Html $css = @' :root{--ink:#1c2530;--muted:#5b6b7b;--accent:#0b6bcb;--rule:#dde5ec;--codebg:#f4f7fa;} *{box-sizing:border-box;} body{font-family:"Segoe UI",Arial,sans-serif;color:var(--ink);line-height:1.55;font-size:12.5px;max-width:800px;margin:0 auto;padding:8px 4px;} h1{font-size:26px;border-bottom:3px solid var(--accent);padding-bottom:6px;margin-top:0;} h2{font-size:19px;margin-top:26px;border-bottom:1px solid var(--rule);padding-bottom:3px;} h3{font-size:15px;margin-top:18px;color:#243447;} code{background:var(--codebg);padding:1px 5px;border-radius:4px;font-family:"Cascadia Code",Consolas,monospace;font-size:11.5px;} pre{background:var(--codebg);border:1px solid var(--rule);border-radius:6px;padding:10px 12px;overflow:auto;} pre code{background:none;padding:0;} table{border-collapse:collapse;width:100%;margin:12px 0;font-size:11.5px;} th,td{border:1px solid var(--rule);padding:6px 9px;text-align:left;vertical-align:top;} th{background:#eef4fb;} blockquote{border-left:4px solid var(--accent);margin:12px 0;padding:4px 14px;color:var(--muted);background:#f7fafd;} img,svg{max-width:100%;height:auto;} a{color:var(--accent);text-decoration:none;} hr{border:none;border-top:1px solid var(--rule);margin:22px 0;} @page{margin:14mm 12mm;} '@ $doc = "<!DOCTYPE html><html lang='en'><head><meta charset='utf-8'><title>$Title</title><style>$css</style></head><body>$bodyHtml</body></html>" $dir = Split-Path -Parent (Resolve-Path -LiteralPath $MdPath) $htmlPath = Join-Path $dir ((Split-Path -Leaf $MdPath) -replace '\.md$', '.gen.html') [System.IO.File]::WriteAllText($htmlPath, $doc, (New-Object System.Text.UTF8Encoding($true))) if (-not $PdfPath) { $PdfPath = ($MdPath -replace '\.md$', '.pdf') } $edge = @( "$env:ProgramFiles\Microsoft\Edge\Application\msedge.exe", "${env:ProgramFiles(x86)}\Microsoft\Edge\Application\msedge.exe" ) | Where-Object { Test-Path $_ } | Select-Object -First 1 if (-not $edge) { throw 'Microsoft Edge not found for PDF rendering.' } $fileUri = 'file:///' + ($htmlPath -replace '\\', '/') # Render to a temp file, then move it into place. Writing straight to $PdfPath # fails silently when the target already exists and is held open (a PDF viewer is # enough), which the old Test-Path check could never detect -- it saw the STALE # file and reported success. Rendering fresh and comparing makes a no-op visible. $tempPdf = Join-Path ([System.IO.Path]::GetTempPath()) ("scfpdf-" + [guid]::NewGuid().ToString('N') + ".pdf") & $edge --headless=new --disable-gpu --no-pdf-header-footer "--print-to-pdf=$tempPdf" $fileUri 2>$null # Edge writes asynchronously; wait for the file to appear AND stop growing. $deadline = (Get-Date).AddSeconds(60) $lastSize = -1 while ((Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 500 if (-not (Test-Path $tempPdf)) { continue } $size = (Get-Item $tempPdf).Length if ($size -gt 0 -and $size -eq $lastSize) { break } $lastSize = $size } if (-not (Test-Path $tempPdf) -or (Get-Item $tempPdf).Length -eq 0) { "PDF NOT created -- Edge produced nothing. Re-run without '2>`$null' on the Edge call to see why." return } try { Move-Item -LiteralPath $tempPdf -Destination $PdfPath -Force -ErrorAction Stop "PDF: $PdfPath ($([math]::Round((Get-Item $PdfPath).Length/1kb,1)) KB, written $((Get-Item $PdfPath).LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss')))" } catch { "PDF RENDERED but could NOT replace '$PdfPath': $($_.Exception.Message). Close anything holding it open. The new file is at: $tempPdf" } |