param( [Parameter(Mandatory = $true)] [string]$PlanMarkdown, [Parameter(Mandatory = $true)] [string]$SourceMarkdown, [Parameter(Mandatory = $true)] [string]$OutputDocx ) $ErrorActionPreference = 'Stop' function Escape-XmlText { param([AllowNull()][string]$Text) if ($null -eq $Text) { return '' } return [System.Security.SecurityElement]::Escape($Text) } function Add-Paragraph { param( [System.Collections.Generic.List[string]]$Parts, [string]$Text, [string]$Style = $null ) $styleXml = '' if ($Style) { $styleXml = "" } $Parts.Add("$styleXml$(Escape-XmlText $Text)") } function Add-Table { param( [System.Collections.Generic.List[string]]$Parts, [string[]]$Headers, [object[]]$Rows ) $tbl = New-Object System.Text.StringBuilder [void]$tbl.Append('') [void]$tbl.Append('') [void]$tbl.Append('') foreach ($h in $Headers) { [void]$tbl.Append('') [void]$tbl.Append((Escape-XmlText $h)) [void]$tbl.Append('') } [void]$tbl.Append('') foreach ($row in $Rows) { [void]$tbl.Append('') foreach ($cell in $row) { [void]$tbl.Append('') [void]$tbl.Append((Escape-XmlText ([string]$cell))) [void]$tbl.Append('') } [void]$tbl.Append('') } [void]$tbl.Append('') $Parts.Add($tbl.ToString()) } function Flush-TableBuffer { param( [System.Collections.Generic.List[string]]$Parts, [System.Collections.Generic.List[string]]$Buffer ) if ($Buffer.Count -lt 2) { foreach ($line in $Buffer) { Add-Paragraph -Parts $Parts -Text $line } $Buffer.Clear() return } $header = $Buffer[0].Trim('|').Split('|') | ForEach-Object { $_.Trim() } $rows = @() for ($i = 2; $i -lt $Buffer.Count; $i++) { $rows += ,($Buffer[$i].Trim('|').Split('|') | ForEach-Object { $_.Trim() }) } Add-Table -Parts $Parts -Headers $header -Rows $rows $Buffer.Clear() } function Add-MarkdownContent { param( [System.Collections.Generic.List[string]]$Parts, [string]$MarkdownPath ) $lines = [System.IO.File]::ReadAllLines($MarkdownPath, [System.Text.Encoding]::UTF8) $tableBuffer = New-Object System.Collections.Generic.List[string] $inCode = $false foreach ($line in $lines) { if ($line.TrimStart().StartsWith('```')) { if ($tableBuffer.Count -gt 0) { Flush-TableBuffer -Parts $Parts -Buffer $tableBuffer } $inCode = -not $inCode Add-Paragraph -Parts $Parts -Text $line -Style 'Code' continue } if ($inCode) { Add-Paragraph -Parts $Parts -Text $line -Style 'Code' continue } if ($line.TrimStart().StartsWith('|') -and $line.TrimEnd().EndsWith('|')) { $tableBuffer.Add($line) continue } if ($tableBuffer.Count -gt 0) { Flush-TableBuffer -Parts $Parts -Buffer $tableBuffer } if ($line.StartsWith('### ')) { Add-Paragraph -Parts $Parts -Text $line.Substring(4) -Style 'Heading3' } elseif ($line.StartsWith('## ')) { Add-Paragraph -Parts $Parts -Text $line.Substring(3) -Style 'Heading2' } elseif ($line.StartsWith('# ')) { Add-Paragraph -Parts $Parts -Text $line.Substring(2) -Style 'Heading1' } elseif ($line.Trim().StartsWith('- ')) { Add-Paragraph -Parts $Parts -Text ('ยท ' + $line.Trim().Substring(2)) } elseif ([string]::IsNullOrWhiteSpace($line)) { Add-Paragraph -Parts $Parts -Text '' } else { Add-Paragraph -Parts $Parts -Text $line } } if ($tableBuffer.Count -gt 0) { Flush-TableBuffer -Parts $Parts -Buffer $tableBuffer } } if (-not (Test-Path -LiteralPath $PlanMarkdown)) { throw "Plan markdown not found: $PlanMarkdown" } if (-not (Test-Path -LiteralPath $SourceMarkdown)) { throw "Source markdown not found: $SourceMarkdown" } $outputDir = Split-Path -Path $OutputDocx -Parent if (-not (Test-Path -LiteralPath $outputDir)) { New-Item -ItemType Directory -Path $outputDir | Out-Null } $tempRoot = Join-Path $env:TEMP ("docx_" + [Guid]::NewGuid().ToString('N')) $wordDir = Join-Path $tempRoot 'word' $relsDir = Join-Path $tempRoot '_rels' $wordRelsDir = Join-Path $wordDir '_rels' New-Item -ItemType Directory -Path $wordDir, $relsDir, $wordRelsDir | Out-Null $parts = New-Object System.Collections.Generic.List[string] Add-MarkdownContent -Parts $parts -MarkdownPath $PlanMarkdown Add-MarkdownContent -Parts $parts -MarkdownPath $SourceMarkdown $documentXml = @" $($parts -join "`n") "@ $stylesXml = @' '@ $contentTypesXml = @' '@ $relsXml = @' '@ $docRelsXml = @' '@ [System.IO.File]::WriteAllText((Join-Path $tempRoot '[Content_Types].xml'), $contentTypesXml, [System.Text.Encoding]::UTF8) [System.IO.File]::WriteAllText((Join-Path $relsDir '.rels'), $relsXml, [System.Text.Encoding]::UTF8) [System.IO.File]::WriteAllText((Join-Path $wordDir 'document.xml'), $documentXml, [System.Text.Encoding]::UTF8) [System.IO.File]::WriteAllText((Join-Path $wordDir 'styles.xml'), $stylesXml, [System.Text.Encoding]::UTF8) [System.IO.File]::WriteAllText((Join-Path $wordRelsDir 'document.xml.rels'), $docRelsXml, [System.Text.Encoding]::UTF8) if (Test-Path -LiteralPath $OutputDocx) { Remove-Item -LiteralPath $OutputDocx -Force } Add-Type -AssemblyName System.IO.Compression.FileSystem [System.IO.Compression.ZipFile]::CreateFromDirectory($tempRoot, $OutputDocx) Remove-Item -LiteralPath $tempRoot -Recurse -Force Write-Output $OutputDocx