Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
14.78% |
17 / 115 |
|
0.00% |
0 / 13 |
CRAP | |
0.00% |
0 / 1 |
CodeDiffHighlighter | |
14.78% |
17 / 115 |
|
0.00% |
0 / 13 |
749.39 | |
0.00% |
0 / 1 |
render | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
splitLines | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
parseLine | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
110 | |||
formatLine | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
56 | |||
handleLineDeletion | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
handleLineAddition | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
handleChunkDelimiter | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
handleUnchanged | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
handleLineFile | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
getLineIdAttr | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
colorLine | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
tagForLine | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
parseChunkDelimiter | |
85.00% |
17 / 20 |
|
0.00% |
0 / 1 |
5.08 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\CodeReview\Backend; |
4 | |
5 | use Exception; |
6 | use Html; |
7 | use Xml; |
8 | |
9 | /** |
10 | * Highlight a SVN diff for easier readibility |
11 | */ |
12 | class CodeDiffHighlighter { |
13 | /** @var int chunk line count for the original file */ |
14 | protected $left = 0; |
15 | /** @var int chunk line count for the changed file */ |
16 | protected $right = 0; |
17 | /** @var int number of chunks */ |
18 | protected $chunk = 0; |
19 | /** @var int line number inside patch */ |
20 | protected $lineNumber = 0; |
21 | |
22 | /** |
23 | * Main entry point. Given a diff text, highlight it |
24 | * and wrap it in a div |
25 | * |
26 | * @param string $text Text to highlight |
27 | * @return string |
28 | */ |
29 | public function render( $text ) { |
30 | return '<table class="mw-codereview-diff">' . |
31 | $this->splitLines( $text ) . |
32 | "</table>\n"; |
33 | } |
34 | |
35 | /** |
36 | * Given a bunch of text, split it into individual |
37 | * lines, color them, then put it back into one big |
38 | * string |
39 | * @param string $text Text to split and highlight |
40 | * @return string |
41 | */ |
42 | public function splitLines( $text ) { |
43 | return implode( "\n", |
44 | array_map( [ $this, 'parseLine' ], |
45 | explode( "\n", $text ) ) ); |
46 | } |
47 | |
48 | /** |
49 | * Internal dispatcher to a handler depending on line |
50 | * Handles lines beginning with '-' '+' '@' and ' ' |
51 | * @param string $line Diff line to parse |
52 | * @return string HTML table line (with <tr></tr>) |
53 | */ |
54 | public function parseLine( $line ) { |
55 | $this->lineNumber++; |
56 | |
57 | if ( $line === '' ) { |
58 | // do not create bogus lines |
59 | return ''; |
60 | } |
61 | |
62 | # Dispatch diff lines to the proper handler |
63 | switch ( substr( $line, 0, 1 ) ) { |
64 | case '-': |
65 | if ( substr( $line, 0, 3 ) === '---' ) { |
66 | return ''; |
67 | } |
68 | $r = $this->handleLineDeletion( $line ); |
69 | break; |
70 | case '+': |
71 | if ( substr( $line, 0, 3 ) === '+++' ) { |
72 | return ''; |
73 | } |
74 | $r = $this->handleLineAddition( $line ); |
75 | break; |
76 | case '@': |
77 | $r = $this->handleChunkDelimiter( $line ); |
78 | break; |
79 | case ' ': |
80 | $r = $this->handleUnchanged( $line ); |
81 | break; |
82 | |
83 | # Patch lines that will be skipped: |
84 | case '=': |
85 | return ''; |
86 | |
87 | # Remaining case should be the file name |
88 | default: |
89 | $r = $this->handleLineFile( $line ); |
90 | } |
91 | |
92 | # Return HTML generated by one of the handler |
93 | return $r; |
94 | } |
95 | |
96 | /** |
97 | * @param string $content |
98 | * @param string|null $class |
99 | * @return string |
100 | */ |
101 | public function formatLine( $content, $class = null ) { |
102 | if ( $class === null ) { |
103 | return Html::rawElement( 'tr', $this->getLineIdAttr(), |
104 | Html::element( 'td', [ 'class' => 'linenumbers' ], $this->left ) . |
105 | Html::element( 'td', [ 'class' => 'linenumbers' ], $this->right ) . |
106 | Html::rawElement( 'td', [], Html::element( 'span', [], $content ) ) |
107 | ); |
108 | } |
109 | |
110 | # Skip line number when they do not apply |
111 | // non-breaking space |
112 | $left = $right = ' '; |
113 | $inlineWrapEl = 'span'; |
114 | |
115 | switch ( $class ) { |
116 | case 'chunkdelimiter': |
117 | // — |
118 | $left = $right = '—'; |
119 | break; |
120 | case 'unchanged': |
121 | $left = $this->left; |
122 | $right = $this->right; |
123 | break; |
124 | case 'del': |
125 | $left = $this->left; |
126 | $inlineWrapEl = 'del'; |
127 | break; |
128 | case 'ins': |
129 | $right = $this->right; |
130 | $inlineWrapEl = 'ins'; |
131 | break; |
132 | |
133 | default: |
134 | # Rely on $left, $right initialization above |
135 | } |
136 | |
137 | $classAttr = [ 'class' => $class ]; |
138 | return Html::rawElement( 'tr', $this->getLineIdAttr(), |
139 | Html::element( 'td', [ 'class' => 'linenumbers' ], $left ) |
140 | . Html::element( 'td', [ 'class' => 'linenumbers' ], $right ) |
141 | . Html::rawElement( 'td', $classAttr, Html::element( $inlineWrapEl, [], $content ) ) |
142 | ); |
143 | } |
144 | |
145 | /** |
146 | * @param string $line |
147 | * @return string |
148 | */ |
149 | public function handleLineDeletion( $line ) { |
150 | $this->left++; |
151 | return $this->formatLine( $line, 'del' ); |
152 | } |
153 | |
154 | /** |
155 | * @param string $line |
156 | * @return string |
157 | */ |
158 | public function handleLineAddition( $line ) { |
159 | $this->right++; |
160 | return $this->formatLine( $line, 'ins' ); |
161 | } |
162 | |
163 | /** |
164 | * @param string $line |
165 | * @return string |
166 | */ |
167 | public function handleChunkDelimiter( $line ) { |
168 | $this->chunk++; |
169 | |
170 | [ |
171 | $this->left, |
172 | # unused |
173 | $leftChanged, |
174 | $this->right, |
175 | # unused |
176 | $rightChanged |
177 | ] = self::parseChunkDelimiter( $line ); |
178 | |
179 | return $this->formatLine( $line, 'chunkdelimiter' ); |
180 | } |
181 | |
182 | /** |
183 | * @param string $line |
184 | * @return string |
185 | */ |
186 | public function handleUnchanged( $line ) { |
187 | $this->left++; |
188 | $this->right++; |
189 | return $this->formatLine( $line, 'unchanged' ); |
190 | } |
191 | |
192 | /** |
193 | * @param string $line |
194 | * @return string |
195 | */ |
196 | public function handleLineFile( $line ) { |
197 | $this->chunk = 0; |
198 | return Html::rawElement( 'tr', |
199 | array_merge( $this->getLineIdAttr(), [ 'class' => 'patchedfile' ] ), |
200 | Html::Element( 'td', [ 'colspan' => 3 ], $line ) |
201 | ); |
202 | } |
203 | |
204 | /** |
205 | * @return array |
206 | */ |
207 | public function getLineIdAttr() { |
208 | return [ 'id' => $this->lineNumber ]; |
209 | } |
210 | |
211 | /** |
212 | * Turn a diff line into a properly formatted string suitable |
213 | * for output |
214 | * @param string $line Line from a diff |
215 | * @return string |
216 | */ |
217 | public function colorLine( $line ) { |
218 | if ( $line == '' ) { |
219 | // don't create bogus spans |
220 | return ''; |
221 | } |
222 | [ $element, $attribs ] = $this->tagForLine( $line ); |
223 | return '<tr>' . Xml::element( $element, $attribs, $line ) . '</tr>'; |
224 | } |
225 | |
226 | /** |
227 | * Take a line of a diff and apply the appropriate stylings |
228 | * @param string $line Line to check |
229 | * @return array |
230 | */ |
231 | public function tagForLine( $line ) { |
232 | static $default = [ 'td', [] ]; |
233 | static $tags = [ |
234 | '-' => [ 'td', [ 'class' => 'del' ] ], |
235 | '+' => [ 'td', [ 'class' => 'ins' ] ], |
236 | '@' => [ 'td', [ 'class' => 'meta' ] ], |
237 | ' ' => [ 'td', [] ], |
238 | ]; |
239 | $first = substr( $line, 0, 1 ); |
240 | if ( isset( $tags[$first] ) ) { |
241 | return $tags[$first]; |
242 | } else { |
243 | return $default; |
244 | } |
245 | } |
246 | |
247 | /** |
248 | * Parse unified diff change chunk header. |
249 | * |
250 | * The format represents two ranges for the left (prefixed with -) and right |
251 | * file (prefixed with +). |
252 | * The format looks like: |
253 | * @@ -l,s +l,s @@ |
254 | * |
255 | * Where: |
256 | * - l is the starting line number |
257 | * - s is the number of lines the change hunk applies to |
258 | * |
259 | * 's', for the number of lines, is optional and default to 1. |
260 | * When omitted, the previous comma will be skipped as well. So all |
261 | * following lines are valid too: |
262 | * |
263 | * @@ -l,s +l @@ |
264 | * @@ -l +l,s @@ |
265 | * @@ -l +l @@ |
266 | * |
267 | * NOTE: visibility is 'public' since the function covered by tests. |
268 | * |
269 | * @param string $chunkHeader a one line chunk as described above |
270 | * @throws Exception |
271 | * @return array with the four values above as an array |
272 | */ |
273 | public static function parseChunkDelimiter( $chunkHeader ) { |
274 | $chunkHeader = rtrim( $chunkHeader ); |
275 | |
276 | # regex snippet to capture a number |
277 | $n = "(\d+)"; |
278 | $s = "(?:,(\d+))"; |
279 | $matches = preg_match( "/^@@ -$n$s \+$n$s @@$/", $chunkHeader, $m ); |
280 | if ( $matches === 1 ) { |
281 | array_shift( $m ); |
282 | return $m; |
283 | } |
284 | |
285 | $s_default_value = 1; |
286 | |
287 | $matches = preg_match( "/^@@ -$n$s \+$n @@$/", $chunkHeader, $m ); |
288 | if ( $matches === 1 ) { |
289 | return [ $m[1], $m[2], $m[3], $s_default_value ]; |
290 | } |
291 | |
292 | $matches = preg_match( "/^@@ -$n \+$n$s @@$/", $chunkHeader, $m ); |
293 | if ( $matches === 1 ) { |
294 | return [ $m[1], $s_default_value, $m[2], $m[3] ]; |
295 | } |
296 | |
297 | $matches = preg_match( "/^@@ -$n \+$n @@$/", $chunkHeader, $m ); |
298 | if ( $matches === 1 ) { |
299 | return [ $m[1], $s_default_value, $m[2], $s_default_value ]; |
300 | } |
301 | |
302 | # We really really should have matched something! |
303 | throw new Exception( |
304 | __METHOD__ . " given an invalid chunk header: '$chunkHeader'\n" |
305 | ); |
306 | } |
307 | } |