210 lines
7.7 KiB
PowerShell
210 lines
7.7 KiB
PowerShell
# Export MagFass2Car guide Markdown to PDF (requires Chrome or Edge)
|
|
param(
|
|
[string]$InputMd = "$PSScriptRoot\MagFass2Car使用指南.md",
|
|
[string]$OutputPdf = "$PSScriptRoot\MagFass2Car使用指南.pdf"
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
|
|
function Escape-Html([string]$text) {
|
|
return [System.Net.WebUtility]::HtmlEncode($text)
|
|
}
|
|
|
|
function Convert-MarkdownToHtml([string]$markdown) {
|
|
$lines = $markdown -split "`r?`n"
|
|
$sb = New-Object System.Text.StringBuilder
|
|
$inCode = $false
|
|
$inTable = $false
|
|
$tableRows = New-Object System.Collections.Generic.List[string[]]
|
|
$inList = $false
|
|
$listOrdered = $false
|
|
|
|
function Flush-Table {
|
|
param($builder, [ref]$inTableRef, $rows)
|
|
if (-not $inTableRef.Value) { return }
|
|
[void]$builder.AppendLine('<table>')
|
|
for ($i = 0; $i -lt $rows.Count; $i++) {
|
|
$cells = $rows[$i]
|
|
$tag = if ($i -eq 0) { 'th' } else { 'td' }
|
|
if ($i -eq 0) { [void]$builder.AppendLine('<thead><tr>') } elseif ($i -eq 1) { [void]$builder.AppendLine('<tbody><tr>') } else { [void]$builder.AppendLine('<tr>') }
|
|
foreach ($cell in $cells) {
|
|
$cellHtml = Escape-Html($cell.Trim())
|
|
$cellHtml = $cellHtml -replace '`([^`]+)`', '<code>$1</code>'
|
|
[void]$builder.AppendLine("<$tag>$cellHtml</$tag>")
|
|
}
|
|
[void]$builder.AppendLine('</tr>')
|
|
if ($i -eq 0) { [void]$builder.AppendLine('</thead>') }
|
|
}
|
|
if ($rows.Count -gt 1) { [void]$builder.AppendLine('</tbody>') }
|
|
[void]$builder.AppendLine('</table>')
|
|
$rows.Clear()
|
|
$inTableRef.Value = $false
|
|
}
|
|
|
|
function Close-List {
|
|
param($builder, [ref]$inListRef, [ref]$orderedRef)
|
|
if (-not $inListRef.Value) { return }
|
|
if ($orderedRef.Value) { [void]$builder.AppendLine('</ol>') } else { [void]$builder.AppendLine('</ul>') }
|
|
$inListRef.Value = $false
|
|
$orderedRef.Value = $false
|
|
}
|
|
|
|
foreach ($line in $lines) {
|
|
if ($line -match '^\s*```') {
|
|
if ($inCode) {
|
|
[void]$sb.AppendLine('</code></pre>')
|
|
$inCode = $false
|
|
} else {
|
|
Flush-Table $sb ([ref]$inTable) $tableRows
|
|
Close-List $sb ([ref]$inList) ([ref]$listOrdered)
|
|
[void]$sb.AppendLine('<pre><code>')
|
|
$inCode = $true
|
|
}
|
|
continue
|
|
}
|
|
|
|
if ($inCode) {
|
|
[void]$sb.AppendLine((Escape-Html $line))
|
|
continue
|
|
}
|
|
|
|
if ($line -match '^\s*\|(.+)\|\s*$') {
|
|
Close-List $sb ([ref]$inList) ([ref]$listOrdered)
|
|
if ($line -match '^\s*\|[-:\s|]+\|\s*$') { continue }
|
|
$cells = ($line.Trim('|') -split '\|') | ForEach-Object { $_.Trim() }
|
|
$tableRows.Add($cells)
|
|
$inTable = $true
|
|
continue
|
|
} else {
|
|
Flush-Table $sb ([ref]$inTable) $tableRows
|
|
}
|
|
|
|
if ($line -match '^(#{1,6})\s+(.+)$') {
|
|
Close-List $sb ([ref]$inList) ([ref]$listOrdered)
|
|
$level = $Matches[1].Length
|
|
$text = $Matches[2]
|
|
[void]$sb.AppendLine("<h$level>$(Escape-Html $text)</h$level>")
|
|
continue
|
|
}
|
|
|
|
if ($line -match '^>\s*(.+)$') {
|
|
Close-List $sb ([ref]$inList) ([ref]$listOrdered)
|
|
$text = $Matches[1]
|
|
[void]$sb.AppendLine("<blockquote><p>$(Escape-Html $text)</p></blockquote>")
|
|
continue
|
|
}
|
|
|
|
if ($line -match '^---\s*$') {
|
|
Close-List $sb ([ref]$inList) ([ref]$listOrdered)
|
|
[void]$sb.AppendLine('<hr/>')
|
|
continue
|
|
}
|
|
|
|
if ($line -match '^\d+\.\s+(.+)$') {
|
|
if (-not $inList) { [void]$sb.AppendLine('<ol>'); $inList = $true; $listOrdered = $true }
|
|
$text = Escape-Html $Matches[1]
|
|
$text = $text -replace '`([^`]+)`', '<code>$1</code>'
|
|
[void]$sb.AppendLine("<li>$text</li>")
|
|
continue
|
|
}
|
|
|
|
if ($line -match '^-\s+(.+)$') {
|
|
if (-not $inList) { [void]$sb.AppendLine('<ul>'); $inList = $true; $listOrdered = $false }
|
|
$text = Escape-Html $Matches[1]
|
|
$text = $text -replace '`([^`]+)`', '<code>$1</code>'
|
|
[void]$sb.AppendLine("<li>$text</li>")
|
|
continue
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($line)) {
|
|
Close-List $sb ([ref]$inList) ([ref]$listOrdered)
|
|
continue
|
|
}
|
|
|
|
Close-List $sb ([ref]$inList) ([ref]$listOrdered)
|
|
$p = Escape-Html $line
|
|
$p = $p -replace '`([^`]+)`', '<code>$1</code>'
|
|
[void]$sb.AppendLine("<p>$p</p>")
|
|
}
|
|
|
|
Flush-Table $sb ([ref]$inTable) $tableRows
|
|
Close-List $sb ([ref]$inList) ([ref]$listOrdered)
|
|
if ($inCode) { [void]$sb.AppendLine('</code></pre>') }
|
|
return $sb.ToString()
|
|
}
|
|
|
|
if (-not (Test-Path $InputMd)) {
|
|
throw "Markdown file not found: $InputMd"
|
|
}
|
|
|
|
$md = Get-Content -Path $InputMd -Raw -Encoding UTF8
|
|
$body = Convert-MarkdownToHtml $md
|
|
$htmlPath = [System.IO.Path]::ChangeExtension($OutputPdf, '.html')
|
|
|
|
$html = @"
|
|
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>MagFass2Car Guide</title>
|
|
<style>
|
|
@page { size: A4; margin: 18mm 16mm; }
|
|
body { font-family: "Microsoft YaHei", "Segoe UI", sans-serif; color: #222; line-height: 1.55; font-size: 11pt; }
|
|
h1 { font-size: 22pt; border-bottom: 2px solid #2563eb; padding-bottom: 8px; margin-top: 0; }
|
|
h2 { font-size: 16pt; color: #1e40af; margin-top: 22px; page-break-after: avoid; }
|
|
h3 { font-size: 13pt; color: #1d4ed8; margin-top: 16px; page-break-after: avoid; }
|
|
h4, h5 { font-size: 11.5pt; margin-top: 12px; page-break-after: avoid; }
|
|
p, li { orphans: 3; widows: 3; }
|
|
code, pre { font-family: Consolas, "Courier New", monospace; }
|
|
code { background: #f3f4f6; padding: 1px 4px; border-radius: 3px; font-size: 10pt; }
|
|
pre { background: #f8fafc; border: 1px solid #e5e7eb; padding: 10px; overflow-x: auto; font-size: 9.5pt; }
|
|
pre code { background: transparent; padding: 0; }
|
|
table { border-collapse: collapse; width: 100%; margin: 10px 0 14px; font-size: 10pt; page-break-inside: avoid; }
|
|
th, td { border: 1px solid #d1d5db; padding: 6px 8px; vertical-align: top; }
|
|
th { background: #eff6ff; text-align: left; }
|
|
tr:nth-child(even) td { background: #fafafa; }
|
|
blockquote { border-left: 4px solid #93c5fd; margin: 10px 0; padding: 6px 12px; color: #374151; background: #f8fafc; }
|
|
hr { border: none; border-top: 1px solid #e5e7eb; margin: 18px 0; }
|
|
ul, ol { margin: 6px 0 10px 20px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
$body
|
|
</body>
|
|
</html>
|
|
"@
|
|
|
|
[System.IO.File]::WriteAllText($htmlPath, $html, [System.Text.UTF8Encoding]::new($false))
|
|
|
|
$browserCandidates = @(
|
|
"$env:ProgramFiles\Google\Chrome\Application\chrome.exe",
|
|
"${env:ProgramFiles(x86)}\Google\Chrome\Application\chrome.exe",
|
|
"${env:ProgramFiles(x86)}\Microsoft\Edge\Application\msedge.exe",
|
|
"$env:ProgramFiles\Microsoft\Edge\Application\msedge.exe"
|
|
)
|
|
$browser = $browserCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
|
|
if (-not $browser) {
|
|
throw "Chrome/Edge not found. HTML generated at: $htmlPath"
|
|
}
|
|
|
|
if (Test-Path $OutputPdf) { Remove-Item $OutputPdf -Force }
|
|
|
|
$resolvedHtml = (Resolve-Path -LiteralPath $htmlPath).Path
|
|
$fileUri = [System.Uri]::new($resolvedHtml).AbsoluteUri
|
|
$args = @(
|
|
"--headless=new",
|
|
"--disable-gpu",
|
|
"--no-pdf-header-footer",
|
|
"--print-to-pdf=$OutputPdf",
|
|
$fileUri
|
|
)
|
|
$proc = Start-Process -FilePath $browser -ArgumentList $args -Wait -PassThru
|
|
|
|
if ($proc.ExitCode -ne 0 -or -not (Test-Path $OutputPdf)) {
|
|
throw "PDF export failed with exit code $($proc.ExitCode). HTML: $htmlPath"
|
|
}
|
|
|
|
Write-Output "PDF: $OutputPdf"
|
|
Write-Output "HTML: $htmlPath"
|