diffContent = file_get_contents($filePath); } public function displaySideBySide(): string { $files = $this->parse(); // 各ファイルにHTMLの出力の作成 $html = ''; foreach ($files as $f) { $old = $f['old'] ?? $f['gitOld']; $new = $f['new'] ?? $f['gitNew']; $name = $old === $new ? $old : $old.' | '.$new; $html .= "\n

ファイル: ".htmlspecialchars($name)."

\n"; if (empty($f['rows'])) { // 内容の差分が無い(バイナリ、名前の変更、モードの変更だけ等) $notes = empty($f['notes']) ? ['変更なし'] : $f['notes']; $html .= "

".htmlspecialchars(implode(' / ', $notes))."

\n"; continue; } $html .= $this->generateHtml($f['rows']); } return $html; } /** * 統一形式(unified diff)を解析し、ファイル毎の行のリストを返す * * @return array ['old', 'new', 'gitOld', 'gitNew', 'notes', 'rows'] の配列 */ private function parse(): array { $files = []; $cur = null; $leftLeft = 0; // 今のハンクで残っている変更前の行数 $rightLeft = 0; // 今のハンクで残っている変更後の行数 $left = 0; // 次の変更前の行番号 $right = 0; // 次の変更後の行番号 $removed = []; $added = []; $flush = function() use (&$cur, &$removed, &$added): void { // 削除と追加の塊を左右に並べる $n = max(count($removed), count($added)); for ($i = 0; $i < $n; $i++) { $cur['rows'][] = [ 'left' => $removed[$i] ?? null, 'right' => $added[$i] ?? null, ]; } $removed = []; $added = []; }; $newFile = function(?string $gitOld, ?string $gitNew) use (&$files, &$cur, $flush): void { if ($cur !== null) { $flush(); $files[] = $cur; } $cur = ['old' => null, 'new' => null, 'gitOld' => $gitOld ?? '?', 'gitNew' => $gitNew ?? '?', 'notes' => [], 'rows' => []]; }; foreach (explode("\n", $this->diffContent) as $raw) { $line = rtrim($raw, "\r"); // ハンクの中:行数で判断する("---" で始まる削除行をファイルヘッダーと間違えない様に) if ($cur !== null && ($leftLeft > 0 || $rightLeft > 0)) { $prefix = substr($line, 0, 1); $content = substr($raw, 1); if ($prefix === ' ' || $line === '') { $flush(); $cur['rows'][] = [ 'left' => ['line' => $left++, 'type' => 'context', 'content' => $content], 'right' => ['line' => $right++, 'type' => 'context', 'content' => $content], ]; $leftLeft--; $rightLeft--; continue; } if ($prefix === '-') { $removed[] = ['line' => $left++, 'type' => 'removed', 'content' => $content]; $leftLeft--; continue; } if ($prefix === '+') { $added[] = ['line' => $right++, 'type' => 'added', 'content' => $content]; $rightLeft--; continue; } if ($prefix === '\\') { continue; // "\ No newline at end of file" } // 想定外の行:ハンクを終わらせて、下で普通に処理する $leftLeft = 0; $rightLeft = 0; } // ハンクの最後の行の直後の "\ No newline at end of file" if (substr($line, 0, 1) === '\\') continue; // ファイルの始まり(git形式) if (preg_match('#^diff --git a/(.+) b/(.+)$#', $line, $m)) { $newFile('a/'.$m[1], 'b/'.$m[2]); continue; } // ファイルヘッダー(git以外のdiffでは、此処がファイルの始まり) if (preg_match('/^--- (.+?)(\t.*)?$/', $line, $m)) { if ($cur === null || $cur['old'] !== null || !empty($cur['rows'])) $newFile(null, null); $cur['old'] = $m[1]; continue; } if (preg_match('/^\+\+\+ (.+?)(\t.*)?$/', $line, $m)) { if ($cur === null) $newFile(null, null); $cur['new'] = $m[1]; continue; } if ($cur === null) continue; // ハンクヘッダー(例:@@ -10,6 +10,7 @@、行数が1の時は ",1" が省略される) if (preg_match('/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/', $line, $m)) { $flush(); $left = (int)$m[1]; $leftLeft = ($m[2] ?? '') === '' ? 1 : (int)$m[2]; $right = (int)$m[3]; $rightLeft = ($m[4] ?? '') === '' ? 1 : (int)$m[4]; // 行数0のハンク(新しい・削除されたファイル)では、番号は次の行から if ($leftLeft === 0) $left++; if ($rightLeft === 0) $right++; $cur['rows'][] = ['hunk' => trim($line)]; continue; } // 内容の無い変更の説明 if (preg_match('/^Binary files (.+) and (.+) differ$/', $line, $m)) { $cur['notes'][] = 'バイナリファイル'; $cur['old'] = $m[1]; $cur['new'] = $m[2]; } elseif (preg_match('/^(new file mode|deleted file mode|old mode|new mode|similarity index|rename from|rename to|copy from|copy to) (.+)$/', $line, $m)) { $labels = [ 'new file mode' => '新しいファイル', 'deleted file mode' => '削除されたファイル', 'old mode' => '前のモード', 'new mode' => '新しいモード', 'similarity index' => '類似度', 'rename from' => '名前の変更元', 'rename to' => '名前の変更先', 'copy from' => 'コピー元', 'copy to' => 'コピー先', ]; $cur['notes'][] = $labels[$m[1]].':'.$m[2]; } } if ($cur !== null) { $flush(); $files[] = $cur; } return $files; } private function generateHtml(array $rows): string { $html = "

\n \n"; $html .= " \n"; foreach ($rows as $row) { if (isset($row['hunk'])) { $html .= " \n"; $html .= " \n"; $html .= " \n"; continue; } $html .= " \n"; $html .= $this->cells($row['left']); // 左(変更前) $html .= $this->cells($row['right']); // 右(変更後) $html .= " \n"; } $html .= "
前新
".htmlspecialchars($row['hunk'])."
\n

\n"; return $html; } private function cells(?array $c): string { if ($c === null) { return "  \n  \n"; } $content = rtrim($c['content'], "\r"); $content = $content === '' ? ' ' : htmlspecialchars($content); return " {$c['line']}\n" . " {$content}\n"; } }