master
ブランチ master
LittleBeast / src / Std / Lib / DiffViewer.php
9.2 KB、232行 生 Blame 履歴
1<?php
2/*************************************************************
3# 076 License
4
5Copyright (c) テクニカル諏訪子
6
7Permission is hereby granted to any person obtaining a copy of the software
8Little Beast (the "Software") to use, modify, merge, copy, publish, distribute,
9sublicense, and/or sell copies of the Software, subject to the following conditions:
10
11 1. **Origin Attribution**:
12 - You must not misrepresent the origin of the Software; you must not claim
13 you created the original Software.
14 - If the Software is used in a product, you must either:
15 a. Provide clear attribution in the product's documentation, user interface,
16 or other visible areas, **OR**
17 b. Pay the original developers a fee they specify in writing.
18 2. **Usage Restriction**:
19 - The Software, or any derivative works, dependencies, or libraries
20 incorporating it, must not be used for censorship or to suppress freedom of
21 speech, expression, or creativity. Prohibited uses include, but are not
22 limited to:
23 - Censorship of so-called "hate speech", visuals, non-mainstream opinions,
24 ideas, or objective reality.
25 - Tools or systems designed to restrict access to information or
26 artistic works.
27 3. **Notice Preservation**:
28 - This license and the above copyright notice must remain intact in all copies
29 of the source code.
30
31THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
32INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
33PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
34HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
35CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
36OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
37 */
38namespace Std\Lib;
39
40class DiffViewer {
41 private $diffContent;
42
43 public function __construct(string $filePath) {
44 if (!file_exists($filePath)) {
45 throw new \Exception("Diff file not found: $filePath");
46 }
47 $this->diffContent = file_get_contents($filePath);
48 }
49
50 public function displaySideBySide(): string {
51 $files = $this->parse();
52
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],
121 ];
122 $leftLeft--;
123 $rightLeft--;
124 continue;
125 }
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]);
149 continue;
150 }
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];
155 continue;
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;
163
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)];
175 continue;
176 }
177
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];
191 }
192 }
193
194 if ($cur !== null) {
195 $flush();
196 $files[] = $cur;
197 }
198
199 return $files;
200 }
201
202 private function generateHtml(array $rows): string {
203 $html = " <p>\n <table class=\"diff-table\">\n";
204 $html .= " <tr class=\"diff-header\"><th colspan=\"2\">前</th><th colspan=\"2\">新</th></tr>\n";
205
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 }
213 $html .= " <tr>\n";
214 $html .= $this->cells($row['left']); // 左(変更前)
215 $html .= $this->cells($row['right']); // 右(変更後)
216 $html .= " </tr>\n";
217 }
218
219 $html .= " </table>\n </p>\n";
220 return $html;
221 }
222
223 private function cells(?array $c): string {
224 if ($c === null) {
225 return " <td class=\"line-number\">&nbsp;</td>\n <td class=\"empty\">&nbsp;</td>\n";
226 }
227 $content = rtrim($c['content'], "\r");
228 $content = $content === '' ? '&nbsp;' : htmlspecialchars($content);
229 return " <td class=\"line-number\">{$c['line']}</td>\n"
230 . " <td class=\"{$c['type']}\">{$content}</td>\n";
231 }
232}