|
1 <?php |
|
2 /** |
|
3 * Diff API: WP_Text_Diff_Renderer_Table class |
|
4 * |
|
5 * @package WordPress |
|
6 * @subpackage Diff |
|
7 * @since 4.7.0 |
|
8 */ |
|
9 |
|
10 /** |
|
11 * Table renderer to display the diff lines. |
|
12 * |
|
13 * @since 2.6.0 |
|
14 * @uses Text_Diff_Renderer Extends |
|
15 */ |
|
16 class WP_Text_Diff_Renderer_Table extends Text_Diff_Renderer { |
|
17 |
|
18 /** |
|
19 * @see Text_Diff_Renderer::_leading_context_lines |
|
20 * @var int |
|
21 * @since 2.6.0 |
|
22 */ |
|
23 public $_leading_context_lines = 10000; |
|
24 |
|
25 /** |
|
26 * @see Text_Diff_Renderer::_trailing_context_lines |
|
27 * @var int |
|
28 * @since 2.6.0 |
|
29 */ |
|
30 public $_trailing_context_lines = 10000; |
|
31 |
|
32 /** |
|
33 * Threshold for when a diff should be saved or omitted. |
|
34 * |
|
35 * @var float |
|
36 * @since 2.6.0 |
|
37 */ |
|
38 protected $_diff_threshold = 0.6; |
|
39 |
|
40 /** |
|
41 * Inline display helper object name. |
|
42 * |
|
43 * @var string |
|
44 * @since 2.6.0 |
|
45 */ |
|
46 protected $inline_diff_renderer = 'WP_Text_Diff_Renderer_inline'; |
|
47 |
|
48 /** |
|
49 * Should we show the split view or not |
|
50 * |
|
51 * @var string |
|
52 * @since 3.6.0 |
|
53 */ |
|
54 protected $_show_split_view = true; |
|
55 |
|
56 protected $compat_fields = array( '_show_split_view', 'inline_diff_renderer', '_diff_threshold' ); |
|
57 |
|
58 /** |
|
59 * Constructor - Call parent constructor with params array. |
|
60 * |
|
61 * This will set class properties based on the key value pairs in the array. |
|
62 * |
|
63 * @since 2.6.0 |
|
64 * |
|
65 * @param array $params |
|
66 */ |
|
67 public function __construct( $params = array() ) { |
|
68 parent::__construct( $params ); |
|
69 if ( isset( $params[ 'show_split_view' ] ) ) |
|
70 $this->_show_split_view = $params[ 'show_split_view' ]; |
|
71 } |
|
72 |
|
73 /** |
|
74 * @ignore |
|
75 * |
|
76 * @param string $header |
|
77 * @return string |
|
78 */ |
|
79 public function _startBlock( $header ) { |
|
80 return ''; |
|
81 } |
|
82 |
|
83 /** |
|
84 * @ignore |
|
85 * |
|
86 * @param array $lines |
|
87 * @param string $prefix |
|
88 */ |
|
89 public function _lines( $lines, $prefix=' ' ) { |
|
90 } |
|
91 |
|
92 /** |
|
93 * @ignore |
|
94 * |
|
95 * @param string $line HTML-escape the value. |
|
96 * @return string |
|
97 */ |
|
98 public function addedLine( $line ) { |
|
99 return "<td class='diff-addedline'>{$line}</td>"; |
|
100 |
|
101 } |
|
102 |
|
103 /** |
|
104 * @ignore |
|
105 * |
|
106 * @param string $line HTML-escape the value. |
|
107 * @return string |
|
108 */ |
|
109 public function deletedLine( $line ) { |
|
110 return "<td class='diff-deletedline'>{$line}</td>"; |
|
111 } |
|
112 |
|
113 /** |
|
114 * @ignore |
|
115 * |
|
116 * @param string $line HTML-escape the value. |
|
117 * @return string |
|
118 */ |
|
119 public function contextLine( $line ) { |
|
120 return "<td class='diff-context'>{$line}</td>"; |
|
121 } |
|
122 |
|
123 /** |
|
124 * @ignore |
|
125 * |
|
126 * @return string |
|
127 */ |
|
128 public function emptyLine() { |
|
129 return '<td> </td>'; |
|
130 } |
|
131 |
|
132 /** |
|
133 * @ignore |
|
134 * |
|
135 * @param array $lines |
|
136 * @param bool $encode |
|
137 * @return string |
|
138 */ |
|
139 public function _added( $lines, $encode = true ) { |
|
140 $r = ''; |
|
141 foreach ($lines as $line) { |
|
142 if ( $encode ) { |
|
143 $processed_line = htmlspecialchars( $line ); |
|
144 |
|
145 /** |
|
146 * Contextually filters a diffed line. |
|
147 * |
|
148 * Filters TextDiff processing of diffed line. By default, diffs are processed with |
|
149 * htmlspecialchars. Use this filter to remove or change the processing. Passes a context |
|
150 * indicating if the line is added, deleted or unchanged. |
|
151 * |
|
152 * @since 4.1.0 |
|
153 * |
|
154 * @param String $processed_line The processed diffed line. |
|
155 * @param String $line The unprocessed diffed line. |
|
156 * @param string null The line context. Values are 'added', 'deleted' or 'unchanged'. |
|
157 */ |
|
158 $line = apply_filters( 'process_text_diff_html', $processed_line, $line, 'added' ); |
|
159 } |
|
160 |
|
161 if ( $this->_show_split_view ) { |
|
162 $r .= '<tr>' . $this->emptyLine() . $this->emptyLine() . $this->addedLine( $line ) . "</tr>\n"; |
|
163 } else { |
|
164 $r .= '<tr>' . $this->addedLine( $line ) . "</tr>\n"; |
|
165 } |
|
166 } |
|
167 return $r; |
|
168 } |
|
169 |
|
170 /** |
|
171 * @ignore |
|
172 * |
|
173 * @param array $lines |
|
174 * @param bool $encode |
|
175 * @return string |
|
176 */ |
|
177 public function _deleted( $lines, $encode = true ) { |
|
178 $r = ''; |
|
179 foreach ($lines as $line) { |
|
180 if ( $encode ) { |
|
181 $processed_line = htmlspecialchars( $line ); |
|
182 |
|
183 /** This filter is documented in wp-includes/wp-diff.php */ |
|
184 $line = apply_filters( 'process_text_diff_html', $processed_line, $line, 'deleted' ); |
|
185 } |
|
186 if ( $this->_show_split_view ) { |
|
187 $r .= '<tr>' . $this->deletedLine( $line ) . $this->emptyLine() . $this->emptyLine() . "</tr>\n"; |
|
188 } else { |
|
189 $r .= '<tr>' . $this->deletedLine( $line ) . "</tr>\n"; |
|
190 } |
|
191 |
|
192 } |
|
193 return $r; |
|
194 } |
|
195 |
|
196 /** |
|
197 * @ignore |
|
198 * |
|
199 * @param array $lines |
|
200 * @param bool $encode |
|
201 * @return string |
|
202 */ |
|
203 public function _context( $lines, $encode = true ) { |
|
204 $r = ''; |
|
205 foreach ($lines as $line) { |
|
206 if ( $encode ) { |
|
207 $processed_line = htmlspecialchars( $line ); |
|
208 |
|
209 /** This filter is documented in wp-includes/wp-diff.php */ |
|
210 $line = apply_filters( 'process_text_diff_html', $processed_line, $line, 'unchanged' ); |
|
211 } |
|
212 if ( $this->_show_split_view ) { |
|
213 $r .= '<tr>' . $this->contextLine( $line ) . $this->emptyLine() . $this->contextLine( $line ) . "</tr>\n"; |
|
214 } else { |
|
215 $r .= '<tr>' . $this->contextLine( $line ) . "</tr>\n"; |
|
216 } |
|
217 } |
|
218 return $r; |
|
219 } |
|
220 |
|
221 /** |
|
222 * Process changed lines to do word-by-word diffs for extra highlighting. |
|
223 * |
|
224 * (TRAC style) sometimes these lines can actually be deleted or added rows. |
|
225 * We do additional processing to figure that out |
|
226 * |
|
227 * @since 2.6.0 |
|
228 * |
|
229 * @param array $orig |
|
230 * @param array $final |
|
231 * @return string |
|
232 */ |
|
233 public function _changed( $orig, $final ) { |
|
234 $r = ''; |
|
235 |
|
236 // Does the aforementioned additional processing |
|
237 // *_matches tell what rows are "the same" in orig and final. Those pairs will be diffed to get word changes |
|
238 // match is numeric: an index in other column |
|
239 // match is 'X': no match. It is a new row |
|
240 // *_rows are column vectors for the orig column and the final column. |
|
241 // row >= 0: an indix of the $orig or $final array |
|
242 // row < 0: a blank row for that column |
|
243 list($orig_matches, $final_matches, $orig_rows, $final_rows) = $this->interleave_changed_lines( $orig, $final ); |
|
244 |
|
245 // These will hold the word changes as determined by an inline diff |
|
246 $orig_diffs = array(); |
|
247 $final_diffs = array(); |
|
248 |
|
249 // Compute word diffs for each matched pair using the inline diff |
|
250 foreach ( $orig_matches as $o => $f ) { |
|
251 if ( is_numeric($o) && is_numeric($f) ) { |
|
252 $text_diff = new Text_Diff( 'auto', array( array($orig[$o]), array($final[$f]) ) ); |
|
253 $renderer = new $this->inline_diff_renderer; |
|
254 $diff = $renderer->render( $text_diff ); |
|
255 |
|
256 // If they're too different, don't include any <ins> or <dels> |
|
257 if ( preg_match_all( '!(<ins>.*?</ins>|<del>.*?</del>)!', $diff, $diff_matches ) ) { |
|
258 // length of all text between <ins> or <del> |
|
259 $stripped_matches = strlen(strip_tags( join(' ', $diff_matches[0]) )); |
|
260 // since we count lengith of text between <ins> or <del> (instead of picking just one), |
|
261 // we double the length of chars not in those tags. |
|
262 $stripped_diff = strlen(strip_tags( $diff )) * 2 - $stripped_matches; |
|
263 $diff_ratio = $stripped_matches / $stripped_diff; |
|
264 if ( $diff_ratio > $this->_diff_threshold ) |
|
265 continue; // Too different. Don't save diffs. |
|
266 } |
|
267 |
|
268 // Un-inline the diffs by removing del or ins |
|
269 $orig_diffs[$o] = preg_replace( '|<ins>.*?</ins>|', '', $diff ); |
|
270 $final_diffs[$f] = preg_replace( '|<del>.*?</del>|', '', $diff ); |
|
271 } |
|
272 } |
|
273 |
|
274 foreach ( array_keys($orig_rows) as $row ) { |
|
275 // Both columns have blanks. Ignore them. |
|
276 if ( $orig_rows[$row] < 0 && $final_rows[$row] < 0 ) |
|
277 continue; |
|
278 |
|
279 // If we have a word based diff, use it. Otherwise, use the normal line. |
|
280 if ( isset( $orig_diffs[$orig_rows[$row]] ) ) |
|
281 $orig_line = $orig_diffs[$orig_rows[$row]]; |
|
282 elseif ( isset( $orig[$orig_rows[$row]] ) ) |
|
283 $orig_line = htmlspecialchars($orig[$orig_rows[$row]]); |
|
284 else |
|
285 $orig_line = ''; |
|
286 |
|
287 if ( isset( $final_diffs[$final_rows[$row]] ) ) |
|
288 $final_line = $final_diffs[$final_rows[$row]]; |
|
289 elseif ( isset( $final[$final_rows[$row]] ) ) |
|
290 $final_line = htmlspecialchars($final[$final_rows[$row]]); |
|
291 else |
|
292 $final_line = ''; |
|
293 |
|
294 if ( $orig_rows[$row] < 0 ) { // Orig is blank. This is really an added row. |
|
295 $r .= $this->_added( array($final_line), false ); |
|
296 } elseif ( $final_rows[$row] < 0 ) { // Final is blank. This is really a deleted row. |
|
297 $r .= $this->_deleted( array($orig_line), false ); |
|
298 } else { // A true changed row. |
|
299 if ( $this->_show_split_view ) { |
|
300 $r .= '<tr>' . $this->deletedLine( $orig_line ) . $this->emptyLine() . $this->addedLine( $final_line ) . "</tr>\n"; |
|
301 } else { |
|
302 $r .= '<tr>' . $this->deletedLine( $orig_line ) . "</tr><tr>" . $this->addedLine( $final_line ) . "</tr>\n"; |
|
303 } |
|
304 } |
|
305 } |
|
306 |
|
307 return $r; |
|
308 } |
|
309 |
|
310 /** |
|
311 * Takes changed blocks and matches which rows in orig turned into which rows in final. |
|
312 * |
|
313 * @since 2.6.0 |
|
314 * |
|
315 * @param array $orig Lines of the original version of the text. |
|
316 * @param array $final Lines of the final version of the text. |
|
317 * @return array { |
|
318 * Array containing results of comparing the original text to the final text. |
|
319 * |
|
320 * @type array $orig_matches Associative array of original matches. Index == row |
|
321 * number of `$orig`, value == corresponding row number |
|
322 * of that same line in `$final` or 'x' if there is no |
|
323 * corresponding row (indicating it is a deleted line). |
|
324 * @type array $final_matches Associative array of final matches. Index == row |
|
325 * number of `$final`, value == corresponding row number |
|
326 * of that same line in `$orig` or 'x' if there is no |
|
327 * corresponding row (indicating it is a new line). |
|
328 * @type array $orig_rows Associative array of interleaved rows of `$orig` with |
|
329 * blanks to keep matches aligned with side-by-side diff |
|
330 * of `$final`. A value >= 0 corresponds to index of `$orig`. |
|
331 * Value < 0 indicates a blank row. |
|
332 * @type array $final_rows Associative array of interleaved rows of `$final` with |
|
333 * blanks to keep matches aligned with side-by-side diff |
|
334 * of `$orig`. A value >= 0 corresponds to index of `$final`. |
|
335 * Value < 0 indicates a blank row. |
|
336 * } |
|
337 */ |
|
338 public function interleave_changed_lines( $orig, $final ) { |
|
339 |
|
340 // Contains all pairwise string comparisons. Keys are such that this need only be a one dimensional array. |
|
341 $matches = array(); |
|
342 foreach ( array_keys($orig) as $o ) { |
|
343 foreach ( array_keys($final) as $f ) { |
|
344 $matches["$o,$f"] = $this->compute_string_distance( $orig[$o], $final[$f] ); |
|
345 } |
|
346 } |
|
347 asort($matches); // Order by string distance. |
|
348 |
|
349 $orig_matches = array(); |
|
350 $final_matches = array(); |
|
351 |
|
352 foreach ( $matches as $keys => $difference ) { |
|
353 list($o, $f) = explode(',', $keys); |
|
354 $o = (int) $o; |
|
355 $f = (int) $f; |
|
356 |
|
357 // Already have better matches for these guys |
|
358 if ( isset($orig_matches[$o]) && isset($final_matches[$f]) ) |
|
359 continue; |
|
360 |
|
361 // First match for these guys. Must be best match |
|
362 if ( !isset($orig_matches[$o]) && !isset($final_matches[$f]) ) { |
|
363 $orig_matches[$o] = $f; |
|
364 $final_matches[$f] = $o; |
|
365 continue; |
|
366 } |
|
367 |
|
368 // Best match of this final is already taken? Must mean this final is a new row. |
|
369 if ( isset($orig_matches[$o]) ) |
|
370 $final_matches[$f] = 'x'; |
|
371 |
|
372 // Best match of this orig is already taken? Must mean this orig is a deleted row. |
|
373 elseif ( isset($final_matches[$f]) ) |
|
374 $orig_matches[$o] = 'x'; |
|
375 } |
|
376 |
|
377 // We read the text in this order |
|
378 ksort($orig_matches); |
|
379 ksort($final_matches); |
|
380 |
|
381 // Stores rows and blanks for each column. |
|
382 $orig_rows = $orig_rows_copy = array_keys($orig_matches); |
|
383 $final_rows = array_keys($final_matches); |
|
384 |
|
385 // Interleaves rows with blanks to keep matches aligned. |
|
386 // We may end up with some extraneous blank rows, but we'll just ignore them later. |
|
387 foreach ( $orig_rows_copy as $orig_row ) { |
|
388 $final_pos = array_search($orig_matches[$orig_row], $final_rows, true); |
|
389 $orig_pos = (int) array_search($orig_row, $orig_rows, true); |
|
390 |
|
391 if ( false === $final_pos ) { // This orig is paired with a blank final. |
|
392 array_splice( $final_rows, $orig_pos, 0, -1 ); |
|
393 } elseif ( $final_pos < $orig_pos ) { // This orig's match is up a ways. Pad final with blank rows. |
|
394 $diff_pos = $final_pos - $orig_pos; |
|
395 while ( $diff_pos < 0 ) |
|
396 array_splice( $final_rows, $orig_pos, 0, $diff_pos++ ); |
|
397 } elseif ( $final_pos > $orig_pos ) { // This orig's match is down a ways. Pad orig with blank rows. |
|
398 $diff_pos = $orig_pos - $final_pos; |
|
399 while ( $diff_pos < 0 ) |
|
400 array_splice( $orig_rows, $orig_pos, 0, $diff_pos++ ); |
|
401 } |
|
402 } |
|
403 |
|
404 // Pad the ends with blank rows if the columns aren't the same length |
|
405 $diff_count = count($orig_rows) - count($final_rows); |
|
406 if ( $diff_count < 0 ) { |
|
407 while ( $diff_count < 0 ) |
|
408 array_push($orig_rows, $diff_count++); |
|
409 } elseif ( $diff_count > 0 ) { |
|
410 $diff_count = -1 * $diff_count; |
|
411 while ( $diff_count < 0 ) |
|
412 array_push($final_rows, $diff_count++); |
|
413 } |
|
414 |
|
415 return array($orig_matches, $final_matches, $orig_rows, $final_rows); |
|
416 } |
|
417 |
|
418 /** |
|
419 * Computes a number that is intended to reflect the "distance" between two strings. |
|
420 * |
|
421 * @since 2.6.0 |
|
422 * |
|
423 * @param string $string1 |
|
424 * @param string $string2 |
|
425 * @return int |
|
426 */ |
|
427 public function compute_string_distance( $string1, $string2 ) { |
|
428 // Vectors containing character frequency for all chars in each string |
|
429 $chars1 = count_chars($string1); |
|
430 $chars2 = count_chars($string2); |
|
431 |
|
432 // L1-norm of difference vector. |
|
433 $difference = array_sum( array_map( array($this, 'difference'), $chars1, $chars2 ) ); |
|
434 |
|
435 // $string1 has zero length? Odd. Give huge penalty by not dividing. |
|
436 if ( !$string1 ) |
|
437 return $difference; |
|
438 |
|
439 // Return distance per character (of string1). |
|
440 return $difference / strlen($string1); |
|
441 } |
|
442 |
|
443 /** |
|
444 * @ignore |
|
445 * @since 2.6.0 |
|
446 * |
|
447 * @param int $a |
|
448 * @param int $b |
|
449 * @return int |
|
450 */ |
|
451 public function difference( $a, $b ) { |
|
452 return abs( $a - $b ); |
|
453 } |
|
454 |
|
455 /** |
|
456 * Make private properties readable for backward compatibility. |
|
457 * |
|
458 * @since 4.0.0 |
|
459 * |
|
460 * @param string $name Property to get. |
|
461 * @return mixed Property. |
|
462 */ |
|
463 public function __get( $name ) { |
|
464 if ( in_array( $name, $this->compat_fields ) ) { |
|
465 return $this->$name; |
|
466 } |
|
467 } |
|
468 |
|
469 /** |
|
470 * Make private properties settable for backward compatibility. |
|
471 * |
|
472 * @since 4.0.0 |
|
473 * |
|
474 * @param string $name Property to check if set. |
|
475 * @param mixed $value Property value. |
|
476 * @return mixed Newly-set property. |
|
477 */ |
|
478 public function __set( $name, $value ) { |
|
479 if ( in_array( $name, $this->compat_fields ) ) { |
|
480 return $this->$name = $value; |
|
481 } |
|
482 } |
|
483 |
|
484 /** |
|
485 * Make private properties checkable for backward compatibility. |
|
486 * |
|
487 * @since 4.0.0 |
|
488 * |
|
489 * @param string $name Property to check if set. |
|
490 * @return bool Whether the property is set. |
|
491 */ |
|
492 public function __isset( $name ) { |
|
493 if ( in_array( $name, $this->compat_fields ) ) { |
|
494 return isset( $this->$name ); |
|
495 } |
|
496 } |
|
497 |
|
498 /** |
|
499 * Make private properties un-settable for backward compatibility. |
|
500 * |
|
501 * @since 4.0.0 |
|
502 * |
|
503 * @param string $name Property to unset. |
|
504 */ |
|
505 public function __unset( $name ) { |
|
506 if ( in_array( $name, $this->compat_fields ) ) { |
|
507 unset( $this->$name ); |
|
508 } |
|
509 } |
|
510 } |