207 lines
10 KiB
PowerShell
207 lines
10 KiB
PowerShell
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 = "<w:pPr><w:pStyle w:val=`"$Style`"/></w:pPr>"
|
|
}
|
|
$Parts.Add("<w:p>$styleXml<w:r><w:t xml:space=`"preserve`">$(Escape-XmlText $Text)</w:t></w:r></w:p>")
|
|
}
|
|
|
|
function Add-Table {
|
|
param(
|
|
[System.Collections.Generic.List[string]]$Parts,
|
|
[string[]]$Headers,
|
|
[object[]]$Rows
|
|
)
|
|
$tbl = New-Object System.Text.StringBuilder
|
|
[void]$tbl.Append('<w:tbl>')
|
|
[void]$tbl.Append('<w:tblPr><w:tblStyle w:val="TableGrid"/><w:tblW w:w="0" w:type="auto"/><w:tblBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:left w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:bottom w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:right w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:insideH w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:insideV w:val="single" w:sz="4" w:space="0" w:color="auto"/></w:tblBorders></w:tblPr>')
|
|
[void]$tbl.Append('<w:tr>')
|
|
foreach ($h in $Headers) {
|
|
[void]$tbl.Append('<w:tc><w:tcPr><w:shd w:val="clear" w:color="auto" w:fill="D9EAF7"/></w:tcPr><w:p><w:r><w:rPr><w:b/></w:rPr><w:t xml:space="preserve">')
|
|
[void]$tbl.Append((Escape-XmlText $h))
|
|
[void]$tbl.Append('</w:t></w:r></w:p></w:tc>')
|
|
}
|
|
[void]$tbl.Append('</w:tr>')
|
|
foreach ($row in $Rows) {
|
|
[void]$tbl.Append('<w:tr>')
|
|
foreach ($cell in $row) {
|
|
[void]$tbl.Append('<w:tc><w:p><w:r><w:t xml:space="preserve">')
|
|
[void]$tbl.Append((Escape-XmlText ([string]$cell)))
|
|
[void]$tbl.Append('</w:t></w:r></w:p></w:tc>')
|
|
}
|
|
[void]$tbl.Append('</w:tr>')
|
|
}
|
|
[void]$tbl.Append('</w:tbl>')
|
|
$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 = @"
|
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
|
<w:body>
|
|
$($parts -join "`n")
|
|
<w:sectPr>
|
|
<w:pgSz w:w="11906" w:h="16838"/>
|
|
<w:pgMar w:top="1440" w:right="1080" w:bottom="1440" w:left="1080" w:header="720" w:footer="720" w:gutter="0"/>
|
|
</w:sectPr>
|
|
</w:body>
|
|
</w:document>
|
|
"@
|
|
|
|
$stylesXml = @'
|
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
|
<w:style w:type="paragraph" w:styleId="Normal"><w:name w:val="Normal"/><w:qFormat/><w:rPr><w:rFonts w:ascii="Microsoft YaHei" w:eastAsia="Microsoft YaHei" w:hAnsi="Microsoft YaHei"/><w:sz w:val="21"/></w:rPr></w:style>
|
|
<w:style w:type="paragraph" w:styleId="Heading1"><w:name w:val="heading 1"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:spacing w:before="360" w:after="120"/><w:outlineLvl w:val="0"/></w:pPr><w:rPr><w:rFonts w:ascii="Microsoft YaHei" w:eastAsia="Microsoft YaHei" w:hAnsi="Microsoft YaHei"/><w:b/><w:sz w:val="30"/></w:rPr></w:style>
|
|
<w:style w:type="paragraph" w:styleId="Heading2"><w:name w:val="heading 2"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:spacing w:before="280" w:after="100"/><w:outlineLvl w:val="1"/></w:pPr><w:rPr><w:rFonts w:ascii="Microsoft YaHei" w:eastAsia="Microsoft YaHei" w:hAnsi="Microsoft YaHei"/><w:b/><w:sz w:val="26"/></w:rPr></w:style>
|
|
<w:style w:type="paragraph" w:styleId="Heading3"><w:name w:val="heading 3"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:spacing w:before="220" w:after="80"/><w:outlineLvl w:val="2"/></w:pPr><w:rPr><w:rFonts w:ascii="Microsoft YaHei" w:eastAsia="Microsoft YaHei" w:hAnsi="Microsoft YaHei"/><w:b/><w:sz w:val="23"/></w:rPr></w:style>
|
|
<w:style w:type="paragraph" w:styleId="Code"><w:name w:val="Code"/><w:basedOn w:val="Normal"/><w:qFormat/><w:rPr><w:rFonts w:ascii="Consolas" w:eastAsia="Microsoft YaHei" w:hAnsi="Consolas"/><w:sz w:val="18"/></w:rPr></w:style>
|
|
<w:style w:type="table" w:styleId="TableGrid"><w:name w:val="Table Grid"/><w:basedOn w:val="TableNormal"/><w:uiPriority w:val="59"/><w:tblPr><w:tblBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:left w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:bottom w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:right w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:insideH w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:insideV w:val="single" w:sz="4" w:space="0" w:color="auto"/></w:tblBorders></w:tblPr></w:style>
|
|
</w:styles>
|
|
'@
|
|
|
|
$contentTypesXml = @'
|
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
|
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
|
<Default Extension="xml" ContentType="application/xml"/>
|
|
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
|
|
<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
|
|
</Types>
|
|
'@
|
|
|
|
$relsXml = @'
|
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
|
|
</Relationships>
|
|
'@
|
|
|
|
$docRelsXml = @'
|
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
|
|
</Relationships>
|
|
'@
|
|
|
|
[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
|