tools/Build-DocsPdf.ps1

#Requires -Version 7.0
<#
.SYNOPSIS
Generate per-doc PDFs from the user-facing Markdown sources.

.DESCRIPTION
Build-DocsPdf.ps1 turns the user-facing Markdown docs into PDF files,
one PDF per Markdown source. The pipeline is fully Windows-native:
PowerShell 7's built-in ConvertFrom-Markdown renders Markdown to HTML
body, the script wraps that body in a styled HTML page (clean
typography, single brand-green accent, restrained), and Microsoft Edge
in headless mode prints the HTML to PDF via --print-to-pdf.

No pandoc, no LaTeX, no Python dependency. The toolchain is whatever
ships with Windows 10+ (Edge) and a recent PowerShell (7+).

Called automatically by tools/Publish-GitEasy.ps1 against the staged
docs folder before the .nupkg contents are listed - PDFs are derived
artifacts and live in the package, not in the source tree.

Can also be run standalone against the source-tree docs (the PDFs
land alongside the .md files); useful for local preview.

.PARAMETER ProjectRoot
Absolute path to the GitEasy source repository. Defaults to
C:\Sysadmin\Scripts\GitEasy.

.PARAMETER OutputDir
Where the generated PDFs land. Defaults to <ProjectRoot>\docs.

.PARAMETER Docs
Relative paths (under ProjectRoot) of the Markdown files to render.
Defaults to the five user-facing docs: HOW-TO, QUICKSTART,
COMMAND_EXAMPLES, GITEASY-VS-RAW-GIT, FOR-GIT-EXPERTS.

.PARAMETER BrowserPath
Override the auto-detected browser. Useful for custom Chrome / Edge
installs. Defaults to msedge.exe at its standard install path, falling
back to chrome.exe.

.EXAMPLE
.\tools\Build-DocsPdf.ps1

Generate PDFs for all five user-facing docs, landing in docs\*.pdf.

.EXAMPLE
.\tools\Build-DocsPdf.ps1 -OutputDir C:\Temp\GitEasyPdfs

Generate PDFs to a custom output directory (the publish script does
this with the staged docs folder as target).

.NOTES
PowerShell 7+ required (ConvertFrom-Markdown was added in PS 6).
Image references in the Markdown are resolved relative to the
generated temp HTML's location, so the temp file lives in the same
folder as the source Markdown during render to keep image links
intact.
#>


[CmdletBinding()]
param(
    [string]   $ProjectRoot = 'C:\Sysadmin\Scripts\GitEasy',
    [string]   $OutputDir,
    [string[]] $Docs = @(
        'docs\HOW-TO-USE-GITEASY.md'
        'docs\QUICKSTART.md'
        'docs\COMMAND_EXAMPLES.md'
        'docs\GITEASY-VS-RAW-GIT.md'
        'docs\FOR-GIT-EXPERTS.md'
    ),
    [string]   $BrowserPath
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

Write-Host ''
Write-Host 'STATE CHECK: GitEasy doc-to-PDF build' -ForegroundColor Cyan

# Sanity
if (-not (Test-Path -LiteralPath $ProjectRoot -PathType Container)) {
    throw "Missing project folder: $ProjectRoot"
}
if (-not $OutputDir) { $OutputDir = Join-Path $ProjectRoot 'docs' }
if (-not (Test-Path -LiteralPath $OutputDir -PathType Container)) {
    New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
}

# Locate the browser
if (-not $BrowserPath) {
    $candidates = @(
        "$env:ProgramFiles\Microsoft\Edge\Application\msedge.exe",
        "${env:ProgramFiles(x86)}\Microsoft\Edge\Application\msedge.exe",
        "$env:ProgramFiles\Google\Chrome\Application\chrome.exe",
        "${env:ProgramFiles(x86)}\Google\Chrome\Application\chrome.exe"
    )
    $BrowserPath = $candidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
}
if (-not $BrowserPath -or -not (Test-Path -LiteralPath $BrowserPath)) {
    throw "Edge or Chrome not found. Set -BrowserPath explicitly or install Microsoft Edge."
}
Write-Host "Browser : $BrowserPath"
Write-Host "Output dir : $OutputDir"
Write-Host "Source docs : $($Docs.Count)"
Write-Host ''

# Embedded CSS. Restrained typography, single brand-green accent,
# generous line-height and whitespace. Tuned for printed/PDF reading.
# HERE-STRING AUDIT (GitEasy DR-011 amended 2026-05-28) — excluded from the
# project's "no here-strings in generated files" rule per suite-policy #2:
# this is a literal CSS template body, single-quoted (never interpolated),
# embedded in a docs-build tool (not in a generated module file). Tool-time
# only; never ships to PSGallery.
$css = @'
@page {
  size: Letter;
  margin: 0.85in 0.75in;
}
body {
  font-family: -apple-system, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
  font-size: 11pt;
  line-height: 1.55;
  color: #111827;
  max-width: 6.75in;
  margin: 0 auto;
  padding: 0;
}
h1 {
  font-size: 24pt;
  font-weight: 700;
  border-bottom: 3px solid #166534;
  padding-bottom: 0.18em;
  margin: 1.6em 0 0.6em;
}
h1:first-child { margin-top: 0; }
h2 {
  font-size: 16pt;
  font-weight: 700;
  border-bottom: 1px solid #d1d5db;
  padding-bottom: 0.12em;
  margin: 1.6em 0 0.5em;
  color: #166534;
}
h3 {
  font-size: 13pt;
  font-weight: 600;
  margin: 1.3em 0 0.4em;
}
h4 {
  font-size: 11.5pt;
  font-weight: 600;
  margin: 1.1em 0 0.3em;
  color: #374151;
}
p { margin: 0.6em 0; }
ul, ol { margin: 0.6em 0; padding-left: 1.4em; }
li { margin: 0.18em 0; }
code {
  font-family: "Cascadia Mono", Consolas, "Courier New", monospace;
  font-size: 10pt;
  background-color: #f3f4f6;
  padding: 0.12em 0.35em;
  border-radius: 3px;
  color: #111827;
}
pre {
  background-color: #f9fafb;
  border: 1px solid #e5e7eb;
  border-left: 3px solid #166534;
  border-radius: 4px;
  padding: 0.7em 0.9em;
  overflow-x: auto;
  page-break-inside: avoid;
}
pre code {
  background: none;
  padding: 0;
  font-size: 9.5pt;
  line-height: 1.45;
}
blockquote {
  border-left: 3px solid #d1d5db;
  padding: 0.1em 0.9em;
  margin: 0.7em 0;
  color: #4b5563;
  font-style: normal;
}
table {
  border-collapse: collapse;
  width: 100%;
  margin: 0.7em 0;
  page-break-inside: avoid;
  font-size: 10pt;
}
th {
  text-align: left;
  border-bottom: 2px solid #166534;
  padding: 0.4em 0.6em;
  background-color: #f9fafb;
}
td {
  border-bottom: 1px solid #e5e7eb;
  padding: 0.35em 0.6em;
  vertical-align: top;
}
a { color: #166534; text-decoration: none; border-bottom: 1px dotted #166534; }
img { max-width: 100%; height: auto; page-break-inside: avoid; }
hr { border: none; border-top: 1px solid #e5e7eb; margin: 1.5em 0; }
strong { font-weight: 600; }
'@


$generated = @()
foreach ($rel in $Docs) {
    $mdPath = Join-Path $ProjectRoot $rel
    if (-not (Test-Path -LiteralPath $mdPath -PathType Leaf)) {
        Write-Warning "Missing source doc, skipping: $rel"
        continue
    }
    $base = [System.IO.Path]::GetFileNameWithoutExtension($mdPath)
    $sourceDir = Split-Path -Parent $mdPath
    $tmpHtml = Join-Path $sourceDir "$base.tmp.html"
    $pdfOut = Join-Path $OutputDir "$base.pdf"

    Write-Host "Rendering $rel" -ForegroundColor Cyan

    # Markdown -> HTML body
    $info = ConvertFrom-Markdown -Path $mdPath
    $bodyHtml = $info.Html

    # Wrap in a styled standalone HTML page. Title is the first H1 if
    # ConvertFrom-Markdown exposed one, falling back to the basename.
    $title = $base
    if ($info.Tokens -and $info.Tokens.Count -gt 0) {
        $firstHeading = $info.Tokens | Where-Object { $_.GetType().Name -eq 'HeadingBlock' -and $_.Level -eq 1 } | Select-Object -First 1
        if ($firstHeading) { $title = $firstHeading.Inline.ToString() }
    }
    # HERE-STRING AUDIT (GitEasy DR-011 amended 2026-05-28) — excluded per
    # suite-policy #2: double-quoted HTML template body. The one interpolated
    # value, $title, is HTML-encoded immediately above via
    # [System.Web.HttpUtility]::HtmlEncode. Tool-time only; output is a
    # docs PDF, never shipped to PSGallery.
    $page = @"
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>$([System.Web.HttpUtility]::HtmlEncode($title))</title>
  <style>
$css
  </style>
</head>
<body>
$bodyHtml
</body>
</html>
"@


    # Write the temp HTML next to the source so relative image refs resolve
    Set-Content -LiteralPath $tmpHtml -Value $page -Encoding UTF8

    # Edge headless print to PDF
    $tmpHtmlUri = ([System.Uri]((Resolve-Path $tmpHtml).Path)).AbsoluteUri
    $browserArgs = @(
        '--headless=new'
        '--disable-gpu'
        '--no-pdf-header-footer'
        "--print-to-pdf=$pdfOut"
        $tmpHtmlUri
    )
    $proc = Start-Process -FilePath $BrowserPath -ArgumentList $browserArgs -Wait -PassThru -WindowStyle Hidden
    if ($proc.ExitCode -ne 0) {
        Write-Warning " Browser exited $($proc.ExitCode) on $rel - PDF may be missing or incomplete."
    }

    # Clean temp HTML
    Remove-Item -LiteralPath $tmpHtml -Force -ErrorAction SilentlyContinue

    if (Test-Path -LiteralPath $pdfOut) {
        $size = (Get-Item $pdfOut).Length
        Write-Host (" OK {0,-30} {1,8:N1} KB" -f "$base.pdf", ($size / 1KB)) -ForegroundColor Green
        $generated += [pscustomobject]@{
            Source = $rel
            Pdf    = $pdfOut
            Bytes  = $size
        }
    } else {
        Write-Warning " PDF not produced: $pdfOut"
    }
}

Write-Host ''
$totalBytes = if ($generated.Count -gt 0) { ($generated | Measure-Object -Property Bytes -Sum).Sum } else { 0 }
Write-Host ("Generated {0} PDF{1}, total {2:N1} KB" -f `
    $generated.Count, $(if ($generated.Count -eq 1) {''} else {'s'}),
    ($totalBytes / 1KB)) -ForegroundColor Cyan

# Emit pipeable results
$generated