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