Public/ConvertTo-DiffHtml.ps1

#Requires -Version 5.1

function ConvertTo-DiffHtml {
    <#
    .SYNOPSIS
        セクションごとの対応付けの結果を、1 つの HTML 文書に変換する。
    .DESCRIPTION
        セクション(比較する対象のファイルなど)ごとに 1 つのカードを作成し、変更前と変更後の
        2 つのペインを左右に並べます。左右のペインのスクロールは、埋め込んだ JavaScript で同期します。
        JavaScript が無効な環境でも、各ペインを個別にスクロールして閲覧できます。

        CSS と JavaScript は HTML に埋め込み、外部のファイルは読み込みません。
        見出しや凡例などの文言は日本語です。
    .PARAMETER Title
        文書の題名。<title> と先頭の見出しに使います。HTML としてエスケープしてから埋め込みます。
    .PARAMETER Sections
        @{ Label = 'アプリ設定'; Rows = <Get-DiffAlignment の戻り値>; Unverified = @('...') }の配列。
        Rows には Get-DiffAlignment の戻り値(TextDiff.DiffRow)だけを渡せます。
        Unverified を指定したセクションは、差分の代わりにその内容を表示します。
        変更前か変更後の取得に失敗し、比較できないセクションに使います。
    .OUTPUTS
        [string] HTML 文書全体。
    .EXAMPLE
        PS> $before = @(Get-Content -LiteralPath .\before.txt -Encoding UTF8)
        PS> $after = @(Get-Content -LiteralPath .\after.txt -Encoding UTF8)
        PS> $rows = Get-DiffAlignment -BeforeLines $before -AfterLines $after
        PS> $html = ConvertTo-DiffHtml -Title 'settings' -Sections @(@{ Label = 'アプリ設定'; Rows = $rows })
        PS> [System.IO.File]::WriteAllText("$PWD\diff.html", $html, [System.Text.UTF8Encoding]::new($false))

        2 つのファイルの差分を、左右に並べた HTML として保存します。
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Title,
        [Parameter(Mandatory)] [AllowEmptyCollection()] [hashtable[]]$Sections
    )

    # Rows の型は、HTML の生成前にすべてのセクションで検証する。
    # 差分の無いセクションは行を出力しないため、Get-DiffHtmlPane の引数の検証を経由しない。
    foreach ($section in $Sections) {
        if (-not $section.ContainsKey('Rows')) { continue }
        $foreignRows = @(@($section.Rows) | Where-Object -FilterScript { $_.PSObject.TypeNames -inotcontains 'TextDiff.DiffRow' })
        if ($foreignRows.Count -gt 0) {
            throw [System.ArgumentException]::new(("Sections の Rows には Get-DiffAlignment の戻り値を渡してください(セクション: {0})" -f $section['Label']), 'Sections')
        }
    }

    $escapedTitle = ConvertTo-HtmlText -Text $Title
    $builder = [System.Text.StringBuilder]::new()

    [void]$builder.AppendLine('<!doctype html>')
    [void]$builder.AppendLine('<html lang="ja">')
    [void]$builder.AppendLine('<head>')
    [void]$builder.AppendLine('<meta charset="utf-8">')
    [void]$builder.AppendLine("<title>$escapedTitle</title>")
    # CSS と JavaScript を埋め込むのは、HTML ファイルを別のマシンにコピーしても表示が崩れないようにするため。
    [void]$builder.AppendLine('<style>')
    [void]$builder.AppendLine((Get-DiffHtmlStyle))
    [void]$builder.AppendLine('</style>')
    [void]$builder.AppendLine('</head>')
    [void]$builder.AppendLine('<body>')
    [void]$builder.AppendLine('<div class="wrap">')
    [void]$builder.AppendLine("<h1>$escapedTitle</h1>")

    foreach ($section in $Sections) {
        $label = ConvertTo-HtmlText -Text ([string]$section.Label)
        [void]$builder.AppendLine('<div class="card">')
        [void]$builder.AppendLine(" <div class=""card-head"">$label</div>")

        if ($section.ContainsKey('Unverified') -and $null -ne $section.Unverified) {
            # 取得に失敗した側があるセクションは、差分を計算できないため理由を表示する。
            # 「差分なし」と区別できない表示にしないため。
            [void]$builder.AppendLine(' <div class="unverified">')
            foreach ($line in @($section.Unverified)) {
                [void]$builder.AppendLine(' <div>' + (ConvertTo-HtmlText -Text $line) + '</div>')
            }
            [void]$builder.AppendLine(' </div>')
            [void]$builder.AppendLine('</div>')
            continue
        }

        $rows = @($section.Rows)
        if (@($rows | Where-Object -FilterScript { $_.Kind -cne 'Same' }).Count -eq 0) {
            [void]$builder.AppendLine(' <div class="nodiff">(差分なし)</div>')
            [void]$builder.AppendLine('</div>')
            continue
        }

        # 変更前と変更後を左右に並べ、片側にしか無い行を斜線で示す。変更・削除・追加を位置で区別できる。
        # 削除行と追加行を上下に並べる表示では、どの行が対応するのか読み取れない。
        # HTML は保存して後から読むため、ConvertTo-DiffText と異なり行を省略しない。
        [void]$builder.AppendLine(' <div class="side"><div>変更前</div><div>変更後</div></div>')
        [void]$builder.AppendLine(' <div class="panes">')
        [void]$builder.AppendLine((Get-DiffHtmlPane -Rows $rows -Side 'left'))
        [void]$builder.AppendLine((Get-DiffHtmlPane -Rows $rows -Side 'right'))
        [void]$builder.AppendLine(' </div>')
        [void]$builder.AppendLine((Get-DiffHtmlLegend))
        [void]$builder.AppendLine('</div>')
    }

    [void]$builder.AppendLine('</div>')
    [void]$builder.AppendLine('<script>')
    [void]$builder.AppendLine((Get-DiffHtmlScript))
    [void]$builder.AppendLine('</script>')
    [void]$builder.AppendLine('</body>')
    [void]$builder.AppendLine('</html>')

    return $builder.ToString()
}