122 lines
4.7 KiB
PowerShell
122 lines
4.7 KiB
PowerShell
param(
|
||||
|
|
[Parameter(Mandatory = $true)]
|
|||
|
|
[string]$Source,
|
|||
|
|
|
|||
|
|
[string]$Output = (Join-Path $PSScriptRoot '..\artifacts\requirements-source-inventory.md')
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
$ErrorActionPreference = 'Stop'
|
|||
|
|
|
|||
|
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|||
|
|
|
|||
|
|
$sourcePath = (Resolve-Path -LiteralPath $Source).Path
|
|||
|
|
$outputPath = [System.IO.Path]::GetFullPath($Output)
|
|||
|
|
$outputDirectory = Split-Path -Parent $outputPath
|
|||
|
|
if (-not (Test-Path -LiteralPath $outputDirectory)) {
|
|||
|
|
New-Item -ItemType Directory -Path $outputDirectory | Out-Null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$stylesByID = @{}
|
|||
|
|
$archive = [System.IO.Compression.ZipFile]::OpenRead($sourcePath)
|
|||
|
|
try {
|
|||
|
|
$entry = $archive.GetEntry('word/document.xml')
|
|||
|
|
if ($null -eq $entry) {
|
|||
|
|
throw 'DOCX中缺少word/document.xml'
|
|||
|
|
}
|
|||
|
|
$stream = $entry.Open()
|
|||
|
|
try {
|
|||
|
|
$reader = [System.IO.StreamReader]::new($stream, [System.Text.Encoding]::UTF8, $true)
|
|||
|
|
try {
|
|||
|
|
[xml]$document = $reader.ReadToEnd()
|
|||
|
|
}
|
|||
|
|
finally {
|
|||
|
|
$reader.Dispose()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
finally {
|
|||
|
|
$stream.Dispose()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$stylesEntry = $archive.GetEntry('word/styles.xml')
|
|||
|
|
if ($null -ne $stylesEntry) {
|
|||
|
|
$stylesStream = $stylesEntry.Open()
|
|||
|
|
try {
|
|||
|
|
$stylesReader = [System.IO.StreamReader]::new($stylesStream, [System.Text.Encoding]::UTF8, $true)
|
|||
|
|
try {
|
|||
|
|
[xml]$stylesDocument = $stylesReader.ReadToEnd()
|
|||
|
|
}
|
|||
|
|
finally {
|
|||
|
|
$stylesReader.Dispose()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
finally {
|
|||
|
|
$stylesStream.Dispose()
|
|||
|
|
}
|
|||
|
|
$stylesNamespace = [System.Xml.XmlNamespaceManager]::new($stylesDocument.NameTable)
|
|||
|
|
$stylesNamespace.AddNamespace('w', 'http://schemas.openxmlformats.org/wordprocessingml/2006/main')
|
|||
|
|
foreach ($styleNode in $stylesDocument.SelectNodes('/w:styles/w:style', $stylesNamespace)) {
|
|||
|
|
$idAttribute = $styleNode.Attributes.GetNamedItem('styleId', 'http://schemas.openxmlformats.org/wordprocessingml/2006/main')
|
|||
|
|
$nameNode = $styleNode.SelectSingleNode('./w:name', $stylesNamespace)
|
|||
|
|
if ($null -eq $idAttribute -or $null -eq $nameNode) {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
$nameAttribute = $nameNode.Attributes.GetNamedItem('val', 'http://schemas.openxmlformats.org/wordprocessingml/2006/main')
|
|||
|
|
if ($null -ne $nameAttribute) {
|
|||
|
|
$stylesByID[$idAttribute.Value] = $nameAttribute.Value
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
finally {
|
|||
|
|
$archive.Dispose()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$namespace = [System.Xml.XmlNamespaceManager]::new($document.NameTable)
|
|||
|
|
$namespace.AddNamespace('w', 'http://schemas.openxmlformats.org/wordprocessingml/2006/main')
|
|||
|
|
$paragraphs = $document.SelectNodes('/w:document/w:body/w:p', $namespace)
|
|||
|
|
if ($paragraphs.Count -eq 0) {
|
|||
|
|
throw 'DOCX正文没有可追踪段落'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$hash = (Get-FileHash -LiteralPath $sourcePath -Algorithm SHA256).Hash.ToLowerInvariant()
|
|||
|
|
$lines = [System.Collections.Generic.List[string]]::new()
|
|||
|
|
$lines.Add('# 原始需求来源段落清单')
|
|||
|
|
$lines.Add('')
|
|||
|
|
$lines.Add('> 此文件由`scripts/extract-requirements.ps1`从原始DOCX机械提取,用于稳定引用来源,不替代原需求文档。`Pxxxx`是`word/document.xml`正文段落的零基顺序号;空段落保留编号但不输出正文。')
|
|||
|
|
$lines.Add('')
|
|||
|
|
$lines.Add("- 来源文件:$([System.IO.Path]::GetFileName($sourcePath))")
|
|||
|
|
$lines.Add(('- SHA-256:`{0}`' -f $hash))
|
|||
|
|
$lines.Add("- 正文段落总数:$($paragraphs.Count)")
|
|||
|
|
$lines.Add('')
|
|||
|
|
$lines.Add('## 非空段落')
|
|||
|
|
$lines.Add('')
|
|||
|
|
|
|||
|
|
$nonEmpty = 0
|
|||
|
|
for ($index = 0; $index -lt $paragraphs.Count; $index++) {
|
|||
|
|
$paragraph = $paragraphs[$index]
|
|||
|
|
$textParts = $paragraph.SelectNodes('.//w:t', $namespace) | ForEach-Object { $_.InnerText }
|
|||
|
|
$text = (($textParts -join '') -replace '[\r\n\t]+', ' ' -replace '\s{2,}', ' ').Trim()
|
|||
|
|
if ([string]::IsNullOrWhiteSpace($text)) {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
$nonEmpty++
|
|||
|
|
$styleNode = $paragraph.SelectSingleNode('./w:pPr/w:pStyle', $namespace)
|
|||
|
|
$style = 'Normal'
|
|||
|
|
if ($null -ne $styleNode) {
|
|||
|
|
$styleAttribute = $styleNode.Attributes.GetNamedItem('val', 'http://schemas.openxmlformats.org/wordprocessingml/2006/main')
|
|||
|
|
if ($null -ne $styleAttribute -and -not [string]::IsNullOrWhiteSpace($styleAttribute.Value)) {
|
|||
|
|
$style = $styleAttribute.Value
|
|||
|
|
if ($stylesByID.ContainsKey($style)) {
|
|||
|
|
$style = $stylesByID[$style]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
$safeText = $text.Replace('\', '\\').Replace('`', '\`')
|
|||
|
|
$lines.Add(('- `P{0}` [{1}] {2}' -f $index.ToString('0000'), $style, $safeText))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$lines.Insert(7, "- 非空段落数:$nonEmpty")
|
|||
|
|
[System.IO.File]::WriteAllLines($outputPath, $lines, [System.Text.UTF8Encoding]::new($false))
|
|||
|
|
|
|||
|
|
Write-Output "已提取 $nonEmpty/$($paragraphs.Count) 个段落:$outputPath"
|