scripts/Convert-CsvToMarkdown.ps1
4.7 KB| 1 | #requires -Version 5.1 |
| 2 | <# |
| 3 | .SYNOPSIS |
| 4 | 将 CSV/TSV 文件转换为 Markdown 表格。 |
| 5 | .DESCRIPTION |
| 6 | 自带 CSV 状态机解析器,支持双引号包裹字段、字段内分隔符与换行、"" 转义引号、 |
| 7 | 自定义分隔符、列对齐、无表头模式,可输出到屏幕或文件。全程离线。 |
| 8 | .PARAMETER InputFile |
| 9 | 输入 CSV 文件路径(必填)。 |
| 10 | .PARAMETER Delimiter |
| 11 | 字段分隔符,单字符,默认逗号。 |
| 12 | .PARAMETER Alignment |
| 13 | 列对齐方式:left、center、right、none,默认 left。 |
| 14 | .PARAMETER NoHeader |
| 15 | 第一行视为数据,自动生成 Column1、Column2 表头。 |
| 16 | .PARAMETER OutFile |
| 17 | 结果写入指定文件(UTF-8 无 BOM);不传则输出到屏幕。 |
| 18 | .PARAMETER KeepWhitespace |
| 19 | 保留字段首尾空白,默认去除。 |
| 20 | .EXAMPLE |
| 21 | .\Convert-CsvToMarkdown.ps1 -InputFile .\data.csv -OutFile .\table.md |
| 22 | #> |
| 23 | [CmdletBinding()] |
| 24 | param( |
| 25 | [Parameter(Mandatory = $true)] |
| 26 | [string]$InputFile, |
| 27 | |
| 28 | [ValidateLength(1, 1)] |
| 29 | [string]$Delimiter = ',', |
| 30 | |
| 31 | [ValidateSet('left', 'center', 'right', 'none')] |
| 32 | [string]$Alignment = 'left', |
| 33 | |
| 34 | [switch]$NoHeader, |
| 35 | [switch]$KeepWhitespace, |
| 36 | [string]$OutFile |
| 37 | ) |
| 38 | |
| 39 | Set-StrictMode -Version 2.0 |
| 40 | |
| 41 | if (-not (Test-Path -LiteralPath $InputFile -PathType Leaf)) { |
| 42 | throw "找不到输入文件:$InputFile" |
| 43 | } |
| 44 | $text = [System.IO.File]::ReadAllText((Resolve-Path -LiteralPath $InputFile).Path) |
| 45 | $delim = [char]$Delimiter |
| 46 | |
| 47 | # 解析整个 CSV 文本(状态机,支持引号内换行) |
| 48 | function Parse-CsvText { |
| 49 | param([string]$Text, [char]$Delimiter) |
| 50 | |
| 51 | $rows = New-Object System.Collections.Generic.List[object] |
| 52 | $row = New-Object System.Collections.Generic.List[string] |
| 53 | $field = New-Object System.Text.StringBuilder |
| 54 | $inQuotes = $false |
| 55 | $i = 0 |
| 56 | |
| 57 | while ($i -lt $Text.Length) { |
| 58 | $c = $Text[$i] |
| 59 | |
| 60 | if ($inQuotes) { |
| 61 | if ($c -eq '"') { |
| 62 | if (($i + 1) -lt $Text.Length -and $Text[$i + 1] -eq '"') { |
| 63 | [void]$field.Append('"'); $i += 2; continue |
| 64 | } |
| 65 | $inQuotes = $false; $i++; continue |
| 66 | } |
| 67 | [void]$field.Append($c); $i++; continue |
| 68 | } |
| 69 | |
| 70 | if ($c -eq '"') { $inQuotes = $true; $i++; continue } |
| 71 | if ($c -eq $Delimiter) { |
| 72 | $row.Add($field.ToString()); [void]$field.Clear(); $i++; continue |
| 73 | } |
| 74 | if ($c -eq "`r" -or $c -eq "`n") { |
| 75 | if ($row.Count -gt 0 -or $field.Length -gt 0) { |
| 76 | $row.Add($field.ToString()); [void]$field.Clear() |
| 77 | $rows.Add($row.ToArray()) |
| 78 | $row = New-Object System.Collections.Generic.List[string] |
| 79 | } |
| 80 | if ($c -eq "`r" -and ($i + 1) -lt $Text.Length -and $Text[$i + 1] -eq "`n") { $i += 2 } |
| 81 | else { $i++ } |
| 82 | continue |
| 83 | } |
| 84 | |
| 85 | [void]$field.Append($c); $i++ |
| 86 | } |
| 87 | |
| 88 | if ($row.Count -gt 0 -or $field.Length -gt 0) { |
| 89 | $row.Add($field.ToString()) |
| 90 | $rows.Add($row.ToArray()) |
| 91 | } |
| 92 | return $rows |
| 93 | } |
| 94 | |
| 95 | $rows = Parse-CsvText -Text $text -Delimiter $delim |
| 96 | if ($rows.Count -eq 0) { |
| 97 | throw '输入文件为空或没有可解析的数据行。' |
| 98 | } |
| 99 | |
| 100 | # 计算最大列数,补齐短行 |
| 101 | $maxCols = 0 |
| 102 | foreach ($r in $rows) { if ($r.Count -gt $maxCols) { $maxCols = $r.Count } } |
| 103 | for ($i = 0; $i -lt $rows.Count; $i++) { |
| 104 | if ($rows[$i].Count -lt $maxCols) { |
| 105 | $fixed = New-Object string[] $maxCols |
| 106 | for ($j = 0; $j -lt $maxCols; $j++) { |
| 107 | $fixed[$j] = if ($j -lt $rows[$i].Count) { $rows[$i][$j] } else { '' } |
| 108 | } |
| 109 | $rows[$i] = $fixed |
| 110 | Write-Warning "第 $($i + 1) 行字段数不足,已用空单元格补齐到 $maxCols 列。" |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | # 表头 |
| 115 | if ($NoHeader) { |
| 116 | $header = @(for ($j = 1; $j -le $maxCols; $j++) { "Column$j" }) |
| 117 | $dataRows = $rows |
| 118 | } else { |
| 119 | $header = $rows[0] |
| 120 | $dataRows = @($rows | Select-Object -Skip 1) |
| 121 | } |
| 122 | |
| 123 | function Convert-EscapeCell { |
| 124 | param([string]$Value) |
| 125 | $v = $Value -replace "`r?`n", ' ' |
| 126 | $v = $v.Replace('|', '\|') |
| 127 | if (-not $KeepWhitespace) { $v = $v.Trim() } |
| 128 | return $v |
| 129 | } |
| 130 | |
| 131 | $separator = switch ($Alignment) { |
| 132 | 'left' { ':---' } |
| 133 | 'center' { ':---:' } |
| 134 | 'right' { '---:' } |
| 135 | 'none' { '---' } |
| 136 | } |
| 137 | |
| 138 | $lines = New-Object System.Collections.Generic.List[string] |
| 139 | $lines.Add('| ' + (($header | ForEach-Object { Convert-EscapeCell $_ }) -join ' | ') + ' |') |
| 140 | $sepCells = @($separator) * $maxCols |
| 141 | $lines.Add('| ' + ($sepCells -join ' | ') + ' |') |
| 142 | foreach ($r in $dataRows) { |
| 143 | $lines.Add('| ' + (($r | ForEach-Object { Convert-EscapeCell $_ }) -join ' | ') + ' |') |
| 144 | } |
| 145 | |
| 146 | $output = ($lines -join "`r`n") |
| 147 | |
| 148 | if ($OutFile) { |
| 149 | [System.IO.File]::WriteAllText($OutFile, $output, (New-Object System.Text.UTF8Encoding($false))) |
| 150 | Write-Output "已写入 $($lines.Count) 行 Markdown 到:$OutFile" |
| 151 | } else { |
| 152 | Write-Output $output |
| 153 | } |
文件内容是平台检查时读出的文本,只供查看。平台不会运行包里的任何脚本或程序。

