source: trunk/forum/includes/diff/renderer.php

Last change on this file was 702, checked in by george, 15 years ago
  • Upraveno: Aktualizace fóra.
File size: 18.4 KB
Line 
1<?php
2/**
3*
4* @package diff
5* @version $Id$
6* @copyright (c) 2006 phpBB Group
7* @license http://opensource.org/licenses/gpl-license.php GNU Public License
8*
9*/
10
11/**
12* @ignore
13*/
14if (!defined('IN_PHPBB'))
15{
16 exit;
17}
18
19/**
20* Code from pear.php.net, Text_Diff-1.1.0 package
21* http://pear.php.net/package/Text_Diff/
22*
23* Modified by phpBB Group to meet our coding standards
24* and being able to integrate into phpBB
25*
26* A class to render Diffs in different formats.
27*
28* This class renders the diff in classic diff format. It is intended that
29* this class be customized via inheritance, to obtain fancier outputs.
30*
31* Copyright 2004-2008 The Horde Project (http://www.horde.org/)
32*
33* @package diff
34*/
35class diff_renderer
36{
37 /**
38 * Number of leading context "lines" to preserve.
39 *
40 * This should be left at zero for this class, but subclasses may want to
41 * set this to other values.
42 */
43 var $_leading_context_lines = 0;
44
45 /**
46 * Number of trailing context "lines" to preserve.
47 *
48 * This should be left at zero for this class, but subclasses may want to
49 * set this to other values.
50 */
51 var $_trailing_context_lines = 0;
52
53 /**
54 * Constructor.
55 */
56 function diff_renderer($params = array())
57 {
58 foreach ($params as $param => $value)
59 {
60 $v = '_' . $param;
61 if (isset($this->$v))
62 {
63 $this->$v = $value;
64 }
65 }
66 }
67
68 /**
69 * Get any renderer parameters.
70 *
71 * @return array All parameters of this renderer object.
72 */
73 function get_params()
74 {
75 $params = array();
76 foreach (get_object_vars($this) as $k => $v)
77 {
78 if ($k[0] == '_')
79 {
80 $params[substr($k, 1)] = $v;
81 }
82 }
83
84 return $params;
85 }
86
87 /**
88 * Renders a diff.
89 *
90 * @param diff &$diff A diff object.
91 *
92 * @return string The formatted output.
93 */
94 function render(&$diff)
95 {
96 $xi = $yi = 1;
97 $block = false;
98 $context = array();
99
100 // Create a new diff object if it is a 3-way diff
101 if (is_a($diff, 'diff3'))
102 {
103 $diff3 = &$diff;
104
105 $diff_1 = $diff3->get_original();
106 $diff_2 = $diff3->merged_output();
107
108 unset($diff3);
109
110 $diff = new diff($diff_1, $diff_2);
111 }
112
113 $nlead = $this->_leading_context_lines;
114 $ntrail = $this->_trailing_context_lines;
115
116 $output = $this->_start_diff();
117 $diffs = $diff->get_diff();
118
119 foreach ($diffs as $i => $edit)
120 {
121 // If these are unchanged (copied) lines, and we want to keep leading or trailing context lines, extract them from the copy block.
122 if (is_a($edit, 'diff_op_copy'))
123 {
124 // Do we have any diff blocks yet?
125 if (is_array($block))
126 {
127 // How many lines to keep as context from the copy block.
128 $keep = ($i == sizeof($diffs) - 1) ? $ntrail : $nlead + $ntrail;
129 if (sizeof($edit->orig) <= $keep)
130 {
131 // We have less lines in the block than we want for context => keep the whole block.
132 $block[] = $edit;
133 }
134 else
135 {
136 if ($ntrail)
137 {
138 // Create a new block with as many lines as we need for the trailing context.
139 $context = array_slice($edit->orig, 0, $ntrail);
140 $block[] = new diff_op_copy($context);
141 }
142
143 $output .= $this->_block($x0, $ntrail + $xi - $x0, $y0, $ntrail + $yi - $y0, $block);
144 $block = false;
145 }
146 }
147 // Keep the copy block as the context for the next block.
148 $context = $edit->orig;
149 }
150 else
151 {
152 // Don't we have any diff blocks yet?
153 if (!is_array($block))
154 {
155 // Extract context lines from the preceding copy block.
156 $context = array_slice($context, sizeof($context) - $nlead);
157 $x0 = $xi - sizeof($context);
158 $y0 = $yi - sizeof($context);
159 $block = array();
160
161 if ($context)
162 {
163 $block[] = new diff_op_copy($context);
164 }
165 }
166 $block[] = $edit;
167 }
168
169 $xi += ($edit->orig) ? sizeof($edit->orig) : 0;
170 $yi += ($edit->final) ? sizeof($edit->final) : 0;
171 }
172
173 if (is_array($block))
174 {
175 $output .= $this->_block($x0, $xi - $x0, $y0, $yi - $y0, $block);
176 }
177
178 return $output . $this->_end_diff();
179 }
180
181 function _block($xbeg, $xlen, $ybeg, $ylen, &$edits)
182 {
183 $output = $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
184
185 foreach ($edits as $edit)
186 {
187 switch (get_class($edit))
188 {
189 case 'diff_op_copy':
190 $output .= $this->_context($edit->orig);
191 break;
192
193 case 'diff_op_add':
194 $output .= $this->_added($edit->final);
195 break;
196
197 case 'diff_op_delete':
198 $output .= $this->_deleted($edit->orig);
199 break;
200
201 case 'diff_op_change':
202 $output .= $this->_changed($edit->orig, $edit->final);
203 break;
204 }
205 }
206
207 return $output . $this->_end_block();
208 }
209
210 function _start_diff()
211 {
212 return '';
213 }
214
215 function _end_diff()
216 {
217 return '';
218 }
219
220 function _block_header($xbeg, $xlen, $ybeg, $ylen)
221 {
222 if ($xlen > 1)
223 {
224 $xbeg .= ',' . ($xbeg + $xlen - 1);
225 }
226
227 if ($ylen > 1)
228 {
229 $ybeg .= ',' . ($ybeg + $ylen - 1);
230 }
231
232 // this matches the GNU Diff behaviour
233 if ($xlen && !$ylen)
234 {
235 $ybeg--;
236 }
237 else if (!$xlen)
238 {
239 $xbeg--;
240 }
241
242 return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
243 }
244
245 function _start_block($header)
246 {
247 return $header . "\n";
248 }
249
250 function _end_block()
251 {
252 return '';
253 }
254
255 function _lines($lines, $prefix = ' ')
256 {
257 return $prefix . implode("\n$prefix", $lines) . "\n";
258 }
259
260 function _context($lines)
261 {
262 return $this->_lines($lines, ' ');
263 }
264
265 function _added($lines)
266 {
267 return $this->_lines($lines, '> ');
268 }
269
270 function _deleted($lines)
271 {
272 return $this->_lines($lines, '< ');
273 }
274
275 function _changed($orig, $final)
276 {
277 return $this->_deleted($orig) . "---\n" . $this->_added($final);
278 }
279
280 /**
281 * Our function to get the diff
282 */
283 function get_diff_content($diff)
284 {
285 return $this->render($diff);
286 }
287}
288
289/**
290* Renders a unified diff
291* @package diff
292*/
293class diff_renderer_unified extends diff_renderer
294{
295 var $_leading_context_lines = 4;
296 var $_trailing_context_lines = 4;
297
298 /**
299 * Our function to get the diff
300 */
301 function get_diff_content($diff)
302 {
303 return nl2br($this->render($diff));
304 }
305
306 function _block_header($xbeg, $xlen, $ybeg, $ylen)
307 {
308 if ($xlen != 1)
309 {
310 $xbeg .= ',' . $xlen;
311 }
312
313 if ($ylen != 1)
314 {
315 $ybeg .= ',' . $ylen;
316 }
317 return '<div class="diff"><big class="info">@@ -' . $xbeg . ' +' . $ybeg . ' @@</big></div>';
318 }
319
320 function _context($lines)
321 {
322 return '<pre class="diff context">' . htmlspecialchars($this->_lines($lines, ' ')) . '<br /></pre>';
323 }
324
325 function _added($lines)
326 {
327 return '<pre class="diff added">' . htmlspecialchars($this->_lines($lines, '+')) . '<br /></pre>';
328 }
329
330 function _deleted($lines)
331 {
332 return '<pre class="diff removed">' . htmlspecialchars($this->_lines($lines, '-')) . '<br /></pre>';
333 }
334
335 function _changed($orig, $final)
336 {
337 return $this->_deleted($orig) . $this->_added($final);
338 }
339
340 function _start_diff()
341 {
342 $start = '<div class="file">';
343
344 return $start;
345 }
346
347 function _end_diff()
348 {
349 return '</div>';
350 }
351
352 function _end_block()
353 {
354 return '';
355 }
356}
357
358/**
359* "Inline" diff renderer.
360*
361* This class renders diffs in the Wiki-style "inline" format.
362*
363* @author Ciprian Popovici
364* @package diff
365*/
366class diff_renderer_inline extends diff_renderer
367{
368 var $_leading_context_lines = 10000;
369 var $_trailing_context_lines = 10000;
370
371 // Prefix and suffix for inserted text
372 var $_ins_prefix = '<span class="ins">';
373 var $_ins_suffix = '</span>';
374
375 // Prefix and suffix for deleted text
376 var $_del_prefix = '<span class="del">';
377 var $_del_suffix = '</span>';
378
379 var $_block_head = '';
380
381 // What are we currently splitting on? Used to recurse to show word-level
382 var $_split_level = 'lines';
383
384 /**
385 * Our function to get the diff
386 */
387 function get_diff_content($diff)
388 {
389 return '<pre>' . nl2br($this->render($diff)) . '<br /></pre>';
390 }
391
392 function _start_diff()
393 {
394 return '';
395 }
396
397 function _end_diff()
398 {
399 return '';
400 }
401
402 function _block_header($xbeg, $xlen, $ybeg, $ylen)
403 {
404 return $this->_block_head;
405 }
406
407 function _start_block($header)
408 {
409 return $header;
410 }
411
412 function _lines($lines, $prefix = ' ', $encode = true)
413 {
414 if ($encode)
415 {
416 array_walk($lines, array(&$this, '_encode'));
417 }
418
419 if ($this->_split_level == 'words')
420 {
421 return implode('', $lines);
422 }
423 else
424 {
425 return implode("\n", $lines) . "\n";
426 }
427 }
428
429 function _added($lines)
430 {
431 array_walk($lines, array(&$this, '_encode'));
432 $lines[0] = $this->_ins_prefix . $lines[0];
433 $lines[sizeof($lines) - 1] .= $this->_ins_suffix;
434 return $this->_lines($lines, ' ', false);
435 }
436
437 function _deleted($lines, $words = false)
438 {
439 array_walk($lines, array(&$this, '_encode'));
440 $lines[0] = $this->_del_prefix . $lines[0];
441 $lines[sizeof($lines) - 1] .= $this->_del_suffix;
442 return $this->_lines($lines, ' ', false);
443 }
444
445 function _changed($orig, $final)
446 {
447 // If we've already split on words, don't try to do so again - just display.
448 if ($this->_split_level == 'words')
449 {
450 $prefix = '';
451 while ($orig[0] !== false && $final[0] !== false && substr($orig[0], 0, 1) == ' ' && substr($final[0], 0, 1) == ' ')
452 {
453 $prefix .= substr($orig[0], 0, 1);
454 $orig[0] = substr($orig[0], 1);
455 $final[0] = substr($final[0], 1);
456 }
457
458 return $prefix . $this->_deleted($orig) . $this->_added($final);
459 }
460
461 $text1 = implode("\n", $orig);
462 $text2 = implode("\n", $final);
463
464 // Non-printing newline marker.
465 $nl = "\0";
466
467 // We want to split on word boundaries, but we need to preserve whitespace as well.
468 // Therefore we split on words, but include all blocks of whitespace in the wordlist.
469 $splitted_text_1 = $this->_split_on_words($text1, $nl);
470 $splitted_text_2 = $this->_split_on_words($text2, $nl);
471
472 $diff = new diff($splitted_text_1, $splitted_text_2);
473 unset($splitted_text_1, $splitted_text_2);
474
475 // Get the diff in inline format.
476 $renderer = new diff_renderer_inline(array_merge($this->get_params(), array('split_level' => 'words')));
477
478 // Run the diff and get the output.
479 return str_replace($nl, "\n", $renderer->render($diff)) . "\n";
480 }
481
482 function _split_on_words($string, $newline_escape = "\n")
483 {
484 // Ignore \0; otherwise the while loop will never finish.
485 $string = str_replace("\0", '', $string);
486
487 $words = array();
488 $length = strlen($string);
489 $pos = 0;
490
491 $tab_there = true;
492 while ($pos < $length)
493 {
494 // Check for tabs... do not include them
495 if ($tab_there && substr($string, $pos, 1) === "\t")
496 {
497 $words[] = "\t";
498 $pos++;
499
500 continue;
501 }
502 else
503 {
504 $tab_there = false;
505 }
506
507 // Eat a word with any preceding whitespace.
508 $spaces = strspn(substr($string, $pos), " \n");
509 $nextpos = strcspn(substr($string, $pos + $spaces), " \n");
510 $words[] = str_replace("\n", $newline_escape, substr($string, $pos, $spaces + $nextpos));
511 $pos += $spaces + $nextpos;
512 }
513
514 return $words;
515 }
516
517 function _encode(&$string)
518 {
519 $string = htmlspecialchars($string);
520 }
521}
522
523/**
524* "raw" diff renderer.
525* This class could be used to output a raw unified patch file
526*
527* @package diff
528*/
529class diff_renderer_raw extends diff_renderer
530{
531 var $_leading_context_lines = 4;
532 var $_trailing_context_lines = 4;
533
534 /**
535 * Our function to get the diff
536 */
537 function get_diff_content($diff)
538 {
539 return '<textarea style="height: 290px;" rows="15" cols="76" class="full">' . htmlspecialchars($this->render($diff)) . '</textarea>';
540 }
541
542 function _block_header($xbeg, $xlen, $ybeg, $ylen)
543 {
544 if ($xlen != 1)
545 {
546 $xbeg .= ',' . $xlen;
547 }
548
549 if ($ylen != 1)
550 {
551 $ybeg .= ',' . $ylen;
552 }
553 return '@@ -' . $xbeg . ' +' . $ybeg . ' @@';
554 }
555
556 function _context($lines)
557 {
558 return $this->_lines($lines, ' ');
559 }
560
561 function _added($lines)
562 {
563 return $this->_lines($lines, '+');
564 }
565
566 function _deleted($lines)
567 {
568 return $this->_lines($lines, '-');
569 }
570
571 function _changed($orig, $final)
572 {
573 return $this->_deleted($orig) . $this->_added($final);
574 }
575}
576
577/**
578* "chora (Horde)" diff renderer - similar style.
579* This renderer class is a modified human_readable function from the Horde Framework.
580*
581* @package diff
582*/
583class diff_renderer_side_by_side extends diff_renderer
584{
585 var $_leading_context_lines = 3;
586 var $_trailing_context_lines = 3;
587
588 var $lines = array();
589
590 // Hold the left and right columns of lines for change blocks.
591 var $cols;
592 var $state;
593
594 var $data = false;
595
596 /**
597 * Our function to get the diff
598 */
599 function get_diff_content($diff)
600 {
601 global $user;
602
603 $output = '';
604 $output .= '<table cellspacing="0" class="hrdiff">
605<caption>
606 <span class="unmodified">&nbsp;</span> ' . $user->lang['LINE_UNMODIFIED'] . '
607 <span class="added">&nbsp;</span> ' . $user->lang['LINE_ADDED'] . '
608 <span class="modified">&nbsp;</span> ' . $user->lang['LINE_MODIFIED'] . '
609 <span class="removed">&nbsp;</span> ' . $user->lang['LINE_REMOVED'] . '
610</caption>
611<tbody>
612';
613
614 $this->render($diff);
615
616 // Is the diff empty?
617 if (!sizeof($this->lines))
618 {
619 $output .= '<tr><th colspan="2">' . $user->lang['NO_VISIBLE_CHANGES'] . '</th></tr>';
620 }
621 else
622 {
623 // Iterate through every header block of changes
624 foreach ($this->lines as $header)
625 {
626 $output .= '<tr><th>' . $user->lang['LINE'] . ' ' . $header['oldline'] . '</th><th>' . $user->lang['LINE'] . ' ' . $header['newline'] . '</th></tr>';
627
628 // Each header block consists of a number of changes (add, remove, change).
629 $current_context = '';
630
631 foreach ($header['contents'] as $change)
632 {
633 if (!empty($current_context) && $change['type'] != 'empty')
634 {
635 $line = $current_context;
636 $current_context = '';
637
638 $output .= '<tr class="unmodified"><td><pre>' . ((strlen($line)) ? $line : '&nbsp;') . '<br /></pre></td>
639 <td><pre>' . ((strlen($line)) ? $line : '&nbsp;') . '<br /></pre></td></tr>';
640 }
641
642 switch ($change['type'])
643 {
644 case 'add':
645 $line = '';
646
647 foreach ($change['lines'] as $_line)
648 {
649 $line .= htmlspecialchars($_line) . '<br />';
650 }
651
652 $output .= '<tr><td class="added_empty">&nbsp;</td><td class="added"><pre>' . ((strlen($line)) ? $line : '&nbsp;') . '<br /></pre></td></tr>';
653 break;
654
655 case 'remove':
656 $line = '';
657
658 foreach ($change['lines'] as $_line)
659 {
660 $line .= htmlspecialchars($_line) . '<br />';
661 }
662
663 $output .= '<tr><td class="removed"><pre>' . ((strlen($line)) ? $line : '&nbsp;') . '<br /></pre></td><td class="removed_empty">&nbsp;</td></tr>';
664 break;
665
666 case 'empty':
667 $current_context .= htmlspecialchars($change['line']) . '<br />';
668 break;
669
670 case 'change':
671 // Pop the old/new stacks one by one, until both are empty.
672 $oldsize = sizeof($change['old']);
673 $newsize = sizeof($change['new']);
674 $left = $right = '';
675
676 for ($row = 0, $row_max = max($oldsize, $newsize); $row < $row_max; ++$row)
677 {
678 $left .= isset($change['old'][$row]) ? htmlspecialchars($change['old'][$row]) : '';
679 $left .= '<br />';
680 $right .= isset($change['new'][$row]) ? htmlspecialchars($change['new'][$row]) : '';
681 $right .= '<br />';
682 }
683
684 $output .= '<tr>';
685
686 if (!empty($left))
687 {
688 $output .= '<td class="modified"><pre>' . $left . '<br /></pre></td>';
689 }
690 else if ($row < $oldsize)
691 {
692 $output .= '<td class="modified">&nbsp;</td>';
693 }
694 else
695 {
696 $output .= '<td class="unmodified">&nbsp;</td>';
697 }
698
699 if (!empty($right))
700 {
701 $output .= '<td class="modified"><pre>' . $right . '<br /></pre></td>';
702 }
703 else if ($row < $newsize)
704 {
705 $output .= '<td class="modified">&nbsp;</td>';
706 }
707 else
708 {
709 $output .= '<td class="unmodified">&nbsp;</td>';
710 }
711
712 $output .= '</tr>';
713 break;
714 }
715 }
716
717 if (!empty($current_context))
718 {
719 $line = $current_context;
720 $current_context = '';
721
722 $output .= '<tr class="unmodified"><td><pre>' . ((strlen($line)) ? $line : '&nbsp;') . '<br /></pre></td>';
723 $output .= '<td><pre>' . ((strlen($line)) ? $line : '&nbsp;') . '<br /></pre></td></tr>';
724 }
725 }
726 }
727
728 $output .= '</tbody></table>';
729
730 return $output;
731 }
732
733 function _start_diff()
734 {
735 $this->lines = array();
736
737 $this->data = false;
738 $this->cols = array(array(), array());
739 $this->state = 'empty';
740
741 return '';
742 }
743
744 function _end_diff()
745 {
746 // Just flush any remaining entries in the columns stack.
747 switch ($this->state)
748 {
749 case 'add':
750 $this->data['contents'][] = array('type' => 'add', 'lines' => $this->cols[0]);
751 break;
752
753 case 'remove':
754 // We have some removal lines pending in our stack, so flush them.
755 $this->data['contents'][] = array('type' => 'remove', 'lines' => $this->cols[0]);
756 break;
757
758 case 'change':
759 // We have both remove and addition lines, so this is a change block.
760 $this->data['contents'][] = array('type' => 'change', 'old' => $this->cols[0], 'new' => $this->cols[1]);
761 break;
762 }
763
764 if ($this->data !== false)
765 {
766 $this->lines[] = $this->data;
767 }
768
769 return '';
770 }
771
772 function _block_header($xbeg, $xlen, $ybeg, $ylen)
773 {
774 // Push any previous header information to the return stack.
775 if ($this->data !== false)
776 {
777 $this->lines[] = $this->data;
778 }
779
780 $this->data = array('type' => 'header', 'oldline' => $xbeg, 'newline' => $ybeg, 'contents' => array());
781 $this->state = 'dump';
782 }
783
784 function _added($lines)
785 {
786 array_walk($lines, array(&$this, '_perform_add'));
787 }
788
789 function _perform_add($line)
790 {
791 if ($this->state == 'empty')
792 {
793 return '';
794 }
795
796 // This is just an addition line.
797 if ($this->state == 'dump' || $this->state == 'add')
798 {
799 // Start adding to the addition stack.
800 $this->cols[0][] = $line;
801 $this->state = 'add';
802 }
803 else
804 {
805 // This is inside a change block, so start accumulating lines.
806 $this->state = 'change';
807 $this->cols[1][] = $line;
808 }
809 }
810
811 function _deleted($lines)
812 {
813 array_walk($lines, array(&$this, '_perform_delete'));
814 }
815
816 function _perform_delete($line)
817 {
818 // This is a removal line.
819 $this->state = 'remove';
820 $this->cols[0][] = $line;
821 }
822
823 function _context($lines)
824 {
825 array_walk($lines, array(&$this, '_perform_context'));
826 }
827
828 function _perform_context($line)
829 {
830 // An empty block with no action.
831 switch ($this->state)
832 {
833 case 'add':
834 $this->data['contents'][] = array('type' => 'add', 'lines' => $this->cols[0]);
835 break;
836
837 case 'remove':
838 // We have some removal lines pending in our stack, so flush them.
839 $this->data['contents'][] = array('type' => 'remove', 'lines' => $this->cols[0]);
840 break;
841
842 case 'change':
843 // We have both remove and addition lines, so this is a change block.
844 $this->data['contents'][] = array('type' => 'change', 'old' => $this->cols[0], 'new' => $this->cols[1]);
845 break;
846 }
847
848 $this->cols = array(array(), array());
849 $this->data['contents'][] = array('type' => 'empty', 'line' => $line);
850 $this->state = 'dump';
851 }
852
853 function _changed($orig, $final)
854 {
855 return $this->_deleted($orig) . $this->_added($final);
856 }
857
858}
859
860?>
Note: See TracBrowser for help on using the repository browser.