ごめん
守矢諏訪子 <suwako@076.moe>、2026-11-10 19:09
コミット b3d0fe015023ef4ac2a13e851c2c3336a8693c1e
・親 de8b809
・ファイルを見る
1個のファイル、+153 -87
| @@ -48,119 +48,185 @@ | ||
| 48 | 48 | } |
| 49 | 49 | |
| 50 | 50 | public function displaySideBySide(): string { |
| 51 | - $lines = explode("\n", $this->diffContent); | |
| 52 | - $fileDiffs = []; | |
| 53 | - $currentFile = null; | |
| 54 | - $hunk = []; | |
| 55 | - $lineNumbers = ['left' => 0, 'right' => 0]; | |
| 56 | - $currentLeftLines = []; | |
| 57 | - $currentRightLines = []; | |
| 51 | + $files = $this->parse(); | |
| 58 | 52 | |
| 59 | - foreach ($lines as $line) { | |
| 60 | - // ファイルヘッダーの処理 | |
| 61 | - if (preg_match('/^---\s+(.+)/', $line, $matches)) { | |
| 62 | - // ファイルを処理する場合、データを保存する | |
| 63 | - if ($currentFile !== null) { | |
| 64 | - $this->processHunk($hunk, $currentLeftLines, $currentRightLines, $lineNumbers); | |
| 65 | - $fileDiffs[$currentFile] = [ | |
| 66 | - 'leftLines' => $currentLeftLines, | |
| 67 | - 'rightLines' => $currentRightLines | |
| 53 | + // 各ファイルにHTMLの出力の作成 | |
| 54 | + $html = ''; | |
| 55 | + foreach ($files as $f) { | |
| 56 | + $old = $f['old'] ?? $f['gitOld']; | |
| 57 | + $new = $f['new'] ?? $f['gitNew']; | |
| 58 | + $name = $old === $new ? $old : $old.' | '.$new; | |
| 59 | + $html .= "\n <h2>ファイル: ".htmlspecialchars($name)."</h2>\n"; | |
| 60 | + if (empty($f['rows'])) { | |
| 61 | + // 内容の差分が無い(バイナリ、名前の変更、モードの変更だけ等) | |
| 62 | + $notes = empty($f['notes']) ? ['変更なし'] : $f['notes']; | |
| 63 | + $html .= " <p>".htmlspecialchars(implode(' / ', $notes))."</p>\n"; | |
| 64 | + continue; | |
| 65 | + } | |
| 66 | + $html .= $this->generateHtml($f['rows']); | |
| 67 | + } | |
| 68 | + | |
| 69 | + return $html; | |
| 70 | + } | |
| 71 | + | |
| 72 | + /** | |
| 73 | + * 統一形式(unified diff)を解析し、ファイル毎の行のリストを返す | |
| 74 | + * | |
| 75 | + * @return array ['old', 'new', 'gitOld', 'gitNew', 'notes', 'rows'] の配列 | |
| 76 | + */ | |
| 77 | + private function parse(): array { | |
| 78 | + $files = []; | |
| 79 | + $cur = null; | |
| 80 | + $leftLeft = 0; // 今のハンクで残っている変更前の行数 | |
| 81 | + $rightLeft = 0; // 今のハンクで残っている変更後の行数 | |
| 82 | + $left = 0; // 次の変更前の行番号 | |
| 83 | + $right = 0; // 次の変更後の行番号 | |
| 84 | + $removed = []; | |
| 85 | + $added = []; | |
| 86 | + | |
| 87 | + $flush = function() use (&$cur, &$removed, &$added): void { | |
| 88 | + // 削除と追加の塊を左右に並べる | |
| 89 | + $n = max(count($removed), count($added)); | |
| 90 | + for ($i = 0; $i < $n; $i++) { | |
| 91 | + $cur['rows'][] = [ | |
| 92 | + 'left' => $removed[$i] ?? null, | |
| 93 | + 'right' => $added[$i] ?? null, | |
| 94 | + ]; | |
| 95 | + } | |
| 96 | + $removed = []; | |
| 97 | + $added = []; | |
| 98 | + }; | |
| 99 | + | |
| 100 | + $newFile = function(?string $gitOld, ?string $gitNew) use (&$files, &$cur, $flush): void { | |
| 101 | + if ($cur !== null) { | |
| 102 | + $flush(); | |
| 103 | + $files[] = $cur; | |
| 104 | + } | |
| 105 | + $cur = ['old' => null, 'new' => null, 'gitOld' => $gitOld ?? '?', 'gitNew' => $gitNew ?? '?', | |
| 106 | + 'notes' => [], 'rows' => []]; | |
| 107 | + }; | |
| 108 | + | |
| 109 | + foreach (explode("\n", $this->diffContent) as $raw) { | |
| 110 | + $line = rtrim($raw, "\r"); | |
| 111 | + | |
| 112 | + // ハンクの中:行数で判断する("---" で始まる削除行をファイルヘッダーと間違えない様に) | |
| 113 | + if ($cur !== null && ($leftLeft > 0 || $rightLeft > 0)) { | |
| 114 | + $prefix = substr($line, 0, 1); | |
| 115 | + $content = substr($raw, 1); | |
| 116 | + if ($prefix === ' ' || $line === '') { | |
| 117 | + $flush(); | |
| 118 | + $cur['rows'][] = [ | |
| 119 | + 'left' => ['line' => $left++, 'type' => 'context', 'content' => $content], | |
| 120 | + 'right' => ['line' => $right++, 'type' => 'context', 'content' => $content], | |
| 68 | 121 | ]; |
| 69 | - $hunk = []; | |
| 70 | - $currentLeftLines = []; | |
| 71 | - $currentRightLines = []; | |
| 72 | - $lineNumbers = ['left' => 0, 'right' => 0]; | |
| 122 | + $leftLeft--; | |
| 123 | + $rightLeft--; | |
| 124 | + continue; | |
| 73 | 125 | } |
| 74 | - $currentFile = $matches[1]; | |
| 126 | + if ($prefix === '-') { | |
| 127 | + $removed[] = ['line' => $left++, 'type' => 'removed', 'content' => $content]; | |
| 128 | + $leftLeft--; | |
| 129 | + continue; | |
| 130 | + } | |
| 131 | + if ($prefix === '+') { | |
| 132 | + $added[] = ['line' => $right++, 'type' => 'added', 'content' => $content]; | |
| 133 | + $rightLeft--; | |
| 134 | + continue; | |
| 135 | + } | |
| 136 | + if ($prefix === '\\') { | |
| 137 | + continue; // "\ No newline at end of file" | |
| 138 | + } | |
| 139 | + // 想定外の行:ハンクを終わらせて、下で普通に処理する | |
| 140 | + $leftLeft = 0; | |
| 141 | + $rightLeft = 0; | |
| 142 | + } | |
| 143 | + // ハンクの最後の行の直後の "\ No newline at end of file" | |
| 144 | + if (substr($line, 0, 1) === '\\') continue; | |
| 145 | + | |
| 146 | + // ファイルの始まり(git形式) | |
| 147 | + if (preg_match('#^diff --git a/(.+) b/(.+)$#', $line, $m)) { | |
| 148 | + $newFile('a/'.$m[1], 'b/'.$m[2]); | |
| 75 | 149 | continue; |
| 76 | 150 | } |
| 77 | - if (preg_match('/^\+\+\+\s+(.+)/', $line)) { | |
| 151 | + // ファイルヘッダー(git以外のdiffでは、此処がファイルの始まり) | |
| 152 | + if (preg_match('/^--- (.+?)(\t.*)?$/', $line, $m)) { | |
| 153 | + if ($cur === null || $cur['old'] !== null || !empty($cur['rows'])) $newFile(null, null); | |
| 154 | + $cur['old'] = $m[1]; | |
| 78 | 155 | continue; |
| 79 | 156 | } |
| 157 | + if (preg_match('/^\+\+\+ (.+?)(\t.*)?$/', $line, $m)) { | |
| 158 | + if ($cur === null) $newFile(null, null); | |
| 159 | + $cur['new'] = $m[1]; | |
| 160 | + continue; | |
| 161 | + } | |
| 162 | + if ($cur === null) continue; | |
| 80 | 163 | |
| 81 | - // ハンクヘッダーの処理 (例:@@ -10,6 +10,7 @@) | |
| 82 | - if (preg_match('/^@@\s+-(\d+),\d+\s+\+(\d+),\d+\s+@@/', $line, $matches)) { | |
| 83 | - $this->processHunk($hunk, $currentLeftLines, $currentRightLines, $lineNumbers); | |
| 84 | - $hunk = []; | |
| 85 | - $lineNumbers['left'] = (int)$matches[1]; | |
| 86 | - $lineNumbers['right'] = (int)$matches[2]; | |
| 164 | + // ハンクヘッダー(例:@@ -10,6 +10,7 @@、行数が1の時は ",1" が省略される) | |
| 165 | + if (preg_match('/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/', $line, $m)) { | |
| 166 | + $flush(); | |
| 167 | + $left = (int)$m[1]; | |
| 168 | + $leftLeft = ($m[2] ?? '') === '' ? 1 : (int)$m[2]; | |
| 169 | + $right = (int)$m[3]; | |
| 170 | + $rightLeft = ($m[4] ?? '') === '' ? 1 : (int)$m[4]; | |
| 171 | + // 行数0のハンク(新しい・削除されたファイル)では、番号は次の行から | |
| 172 | + if ($leftLeft === 0) $left++; | |
| 173 | + if ($rightLeft === 0) $right++; | |
| 174 | + $cur['rows'][] = ['hunk' => trim($line)]; | |
| 87 | 175 | continue; |
| 88 | 176 | } |
| 89 | 177 | |
| 90 | - // ハンクでの行列の集まり | |
| 91 | - if (substr($line, 0, 1) === '-' || substr($line, 0, 1) === '+' || substr($line, 0, 1) === ' ') { | |
| 92 | - $hunk[] = $line; | |
| 178 | + // 内容の無い変更の説明 | |
| 179 | + if (preg_match('/^Binary files (.+) and (.+) differ$/', $line, $m)) { | |
| 180 | + $cur['notes'][] = 'バイナリファイル'; | |
| 181 | + $cur['old'] = $m[1]; | |
| 182 | + $cur['new'] = $m[2]; | |
| 183 | + } 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)) { | |
| 184 | + $labels = [ | |
| 185 | + 'new file mode' => '新しいファイル', 'deleted file mode' => '削除されたファイル', | |
| 186 | + 'old mode' => '前のモード', 'new mode' => '新しいモード', 'similarity index' => '類似度', | |
| 187 | + 'rename from' => '名前の変更元', 'rename to' => '名前の変更先', 'copy from' => 'コピー元', | |
| 188 | + 'copy to' => 'コピー先', | |
| 189 | + ]; | |
| 190 | + $cur['notes'][] = $labels[$m[1]].':'.$m[2]; | |
| 93 | 191 | } |
| 94 | 192 | } |
| 95 | 193 | |
| 96 | - // 最後のハンク・ファイルの処理 | |
| 97 | - if ($currentFile !== null) { | |
| 98 | - $this->processHunk($hunk, $currentLeftLines, $currentRightLines, $lineNumbers); | |
| 99 | - $fileDiffs[$currentFile] = [ | |
| 100 | - 'leftLines' => $currentLeftLines, | |
| 101 | - 'rightLines' => $currentRightLines | |
| 102 | - ]; | |
| 194 | + if ($cur !== null) { | |
| 195 | + $flush(); | |
| 196 | + $files[] = $cur; | |
| 103 | 197 | } |
| 104 | 198 | |
| 105 | - // 各ファイルにHTMLの出力の作成 | |
| 106 | - $html = ''; | |
| 107 | - foreach ($fileDiffs as $fileName => $diff) { | |
| 108 | - $html .= "\n <h2>ファイル: ".htmlspecialchars($fileName)."</h2>\n"; | |
| 109 | - $html .= $this->generateHtml($diff['leftLines'], $diff['rightLines']); | |
| 110 | - } | |
| 111 | - | |
| 112 | - return $html; | |
| 199 | + return $files; | |
| 113 | 200 | } |
| 114 | 201 | |
| 115 | - private function processHunk(array $hunk, array &$leftLines, array &$rightLines, array &$lineNumbers): void { | |
| 116 | - foreach ($hunk as $line) { | |
| 117 | - $prefix = substr($line, 0, 1); | |
| 118 | - $content = substr($line, 1); | |
| 119 | - | |
| 120 | - if ($prefix === '-') { | |
| 121 | - $leftLines[] = ['content' => htmlspecialchars($content), 'type' => 'removed', 'line' => $lineNumbers['left']]; | |
| 122 | - $lineNumbers['left']++; | |
| 123 | - } elseif ($prefix === '+') { | |
| 124 | - $rightLines[] = ['content' => htmlspecialchars($content), 'type' => 'added', 'line' => $lineNumbers['right']]; | |
| 125 | - $lineNumbers['right']++; | |
| 126 | - } elseif ($prefix === ' ') { | |
| 127 | - // 両側のコンテキストは同じ行列があるかの確認 | |
| 128 | - while ($lineNumbers['left'] < $lineNumbers['right']) { | |
| 129 | - $leftLines[] = ['content' => '', 'type' => 'empty', 'line' => $lineNumbers['left']]; | |
| 130 | - $lineNumbers['left']++; | |
| 131 | - } | |
| 132 | - while ($lineNumbers['right'] < $lineNumbers['left']) { | |
| 133 | - $rightLines[] = ['content' => '', 'type' => 'empty', 'line' => $lineNumbers['right']]; | |
| 134 | - $lineNumbers['right']++; | |
| 135 | - } | |
| 136 | - $leftLines[] = ['content' => htmlspecialchars($content), 'type' => 'context', 'line' => $lineNumbers['left']]; | |
| 137 | - $rightLines[] = ['content' => htmlspecialchars($content), 'type' => 'context', 'line' => $lineNumbers['right']]; | |
| 138 | - $lineNumbers['left']++; | |
| 139 | - $lineNumbers['right']++; | |
| 140 | - } | |
| 141 | - } | |
| 142 | - } | |
| 143 | - | |
| 144 | - private function generateHtml(array $leftLines, array $rightLines): string { | |
| 202 | + private function generateHtml(array $rows): string { | |
| 145 | 203 | $html = " <p>\n <table class=\"diff-table\">\n"; |
| 146 | 204 | $html .= " <tr class=\"diff-header\"><th colspan=\"2\">前</th><th colspan=\"2\">新</th></tr>\n"; |
| 147 | 205 | |
| 148 | - $maxLines = max(count($leftLines), count($rightLines)); | |
| 149 | - for ($i = 0; $i < $maxLines; $i++) { | |
| 150 | - $left = isset($leftLines[$i]) ? $leftLines[$i] : ['content' => '', 'type' => 'empty', 'line' => '']; | |
| 151 | - $right = isset($rightLines[$i]) ? $rightLines[$i] : ['content' => '', 'type' => 'empty', 'line' => '']; | |
| 152 | - | |
| 206 | + foreach ($rows as $row) { | |
| 207 | + if (isset($row['hunk'])) { | |
| 208 | + $html .= " <tr class=\"hunk\">\n"; | |
| 209 | + $html .= " <td colspan=\"4\">".htmlspecialchars($row['hunk'])."</td>\n"; | |
| 210 | + $html .= " </tr>\n"; | |
| 211 | + continue; | |
| 212 | + } | |
| 153 | 213 | $html .= " <tr>\n"; |
| 154 | - // 左(変更前) | |
| 155 | - $html .= " <td class=\"line-number\">".($left['line'] ?: ' ')."</td>\n"; | |
| 156 | - $html .= " <td class=\"{$left['type']}\">".($left['content'] ?: ' ')."</td>\n"; | |
| 157 | - // 右(変更後) | |
| 158 | - $html .= " <td class=\"line-number\">".($right['line'] ?: ' ')."</td>\n"; | |
| 159 | - $html .= " <td class=\"{$right['type']}\">".($right['content'] ?: ' ')."</td>\n"; | |
| 214 | + $html .= $this->cells($row['left']); // 左(変更前) | |
| 215 | + $html .= $this->cells($row['right']); // 右(変更後) | |
| 160 | 216 | $html .= " </tr>\n"; |
| 161 | 217 | } |
| 162 | 218 | |
| 163 | 219 | $html .= " </table>\n </p>\n"; |
| 164 | 220 | return $html; |
| 221 | + } | |
| 222 | + | |
| 223 | + private function cells(?array $c): string { | |
| 224 | + if ($c === null) { | |
| 225 | + return " <td class=\"line-number\"> </td>\n <td class=\"empty\"> </td>\n"; | |
| 226 | + } | |
| 227 | + $content = rtrim($c['content'], "\r"); | |
| 228 | + $content = $content === '' ? ' ' : htmlspecialchars($content); | |
| 229 | + return " <td class=\"line-number\">{$c['line']}</td>\n" | |
| 230 | + . " <td class=\"{$c['type']}\">{$content}</td>\n"; | |
| 165 | 231 | } |
| 166 | 232 | } |