]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/difflib.php
New class MappedDiff to support trailing-space insentive diffs (and the like).
[SourceForge/phpwiki.git] / lib / difflib.php
1 <?php
2 rcs_id('$Id: difflib.php,v 1.3 2001-12-15 02:39:43 dairiki Exp $');
3 // difflib.php
4 //
5 // A PHP diff engine for phpwiki.
6 //
7 // Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
8 // You may copy this code freely under the conditions of the GPL.
9 //
10
11 // FIXME: possibly remove assert()'s for production version?
12
13 // PHP3 does not have assert()
14 define('USE_ASSERTS', function_exists('assert'));
15
16 class _DiffOp {
17     var $type;
18     var $orig;
19     var $final;
20     
21     function reverse() {
22         trigger_error("pure virtual", E_USER_ERROR);
23     }
24 }
25
26 class _DiffOp_Copy extends _DiffOp {
27     var $type = 'copy';
28     
29     function _DiffOp_Copy ($orig, $final = false) {
30         if (!is_array($final))
31             $final = $orig;
32         $this->orig = $orig;
33         $this->final = $final;
34     }
35
36     function reverse() {
37         return new _DiffOp_Copy($this->final, $this->orig);
38     }
39 }
40
41 class _DiffOp_Delete extends _DiffOp {
42     var $type = 'delete';
43     
44     function _DiffOp_Delete ($lines) {
45         $this->orig = $lines;
46         $this->final = false;
47     }
48
49     function reverse() {
50         return new _DiffOp_Add($this->orig);
51     }
52 }
53
54 class _DiffOp_Add extends _DiffOp {
55     var $type = 'add';
56     
57     function _DiffOp_Add ($lines) {
58         $this->final = $lines;
59         $this->orig = false;
60     }
61
62     function reverse() {
63         return new _DiffOp_Delete($this->final);
64     }
65 }
66
67 class _DiffOp_Change extends _DiffOp {
68     var $type = 'change';
69     
70     function _DiffOp_Change ($orig, $final) {
71         $this->orig = $orig;
72         $this->final = $final;
73     }
74
75     function reverse() {
76         return new _DiffOp_Change($this->final, $this->orig);
77     }
78 }
79         
80       
81 /**
82  * Class used internally by Diff to actually compute the diffs.
83  *
84  * The algorithm used here is mostly lifted from the perl module
85  * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
86  *   http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
87  *
88  * More ideas are taken from:
89  *   http://www.ics.uci.edu/~eppstein/161/960229.html
90  *
91  * Some ideas are (and a bit of code) are from from analyze.c, from GNU
92  * diffutils-2.7, which can be found at:
93  *   ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
94  *
95  * Finally, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
96  * are my own.
97  *
98  * @author Geoffrey T. Dairiki
99  * @access private
100  */
101 class _DiffEngine
102 {
103     var $xv = array(), $yv = array();
104
105     function diff ($from_lines, $to_lines) {
106         $n_from = sizeof($from_lines);
107         $n_to = sizeof($to_lines);
108
109
110         // Skip leading common lines.
111         for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
112             if ($from_lines[$skip] != $to_lines[$skip])
113                 break;
114             $this->xchanged[$skip] = $this->ychanged[$skip] = false;
115         }
116         // Skip trailing common lines.
117         $xi = $n_from; $yi = $n_to;
118         for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
119             if ($from_lines[$xi] != $to_lines[$yi])
120                 break;
121             $this->xchanged[$xi] = $this->ychanged[$yi] = false;
122         }
123         
124         // Ignore lines which do not exist in both files.
125         for ($xi = $skip; $xi < $n_from - $endskip; $xi++)
126             $xhash[$from_lines[$xi]] = 1;
127         for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
128             $line = $to_lines[$yi];
129             if ( ($this->ychanged[$yi] = empty($xhash[$line])) )
130                 continue;
131             $yhash[$line] = 1;
132             $this->yv[] = $line;
133             $this->yind[] = $yi;
134         }
135         for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
136             $line = $from_lines[$xi];
137             if ( ($this->xchanged[$xi] = empty($yhash[$line])) )
138                 continue;
139             $this->xv[] = $line;
140             $this->xind[] = $xi;
141         }
142
143         // Find the LCS.
144         $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
145
146         // Merge edits when possible
147         $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
148         $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
149
150         // Compute the edit operations.
151         $edits = array();
152         $xi = $yi = 0;
153         while ($xi < $n_from || $yi < $n_to) {
154             USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
155             USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
156
157             // Skip matching "snake".
158             $copy = array();
159             while ( $xi < $n_from && $yi < $n_to
160                     && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
161                 $copy[] = $from_lines[$xi++];
162                 ++$yi;
163             }
164             if ($copy)
165                 $edits[] = new _DiffOp_Copy($copy);
166
167             // Find deletes & adds.
168             $delete = array();
169             while ($xi < $n_from && $this->xchanged[$xi])
170                 $delete[] = $from_lines[$xi++];
171
172             $add = array();
173             while ($yi < $n_to && $this->ychanged[$yi])
174                 $add[] = $to_lines[$yi++];
175             
176             if ($delete && $add)
177                 $edits[] = new _DiffOp_Change($delete, $add);
178             elseif ($delete)
179                 $edits[] = new _DiffOp_Delete($delete);
180             elseif ($add)
181                 $edits[] = new _DiffOp_Add($add);
182         }
183         return $edits;
184     }
185     
186
187     /* Divide the Largest Common Subsequence (LCS) of the sequences
188      * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
189      * sized segments.
190      *
191      * Returns (LCS, PTS).  LCS is the length of the LCS. PTS is an
192      * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
193      * sub sequences.  The first sub-sequence is contained in [X0, X1),
194      * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on.  Note
195      * that (X0, Y0) == (XOFF, YOFF) and
196      * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
197      *
198      * This function assumes that the first lines of the specified portions
199      * of the two files do not match, and likewise that the last lines do not
200      * match.  The caller must trim matching lines from the beginning and end
201      * of the portions it is going to specify.
202      */
203     function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks) {
204         $flip = false;
205         
206         if ($xlim - $xoff > $ylim - $yoff) {
207             // Things seems faster (I'm not sure I understand why)
208             // when the shortest sequence in X.
209             $flip = true;
210             list ($xoff, $xlim, $yoff, $ylim)
211                 = array( $yoff, $ylim, $xoff, $xlim);
212         }
213
214         if ($flip)
215             for ($i = $ylim - 1; $i >= $yoff; $i--)
216                 $ymatches[$this->xv[$i]][] = $i;
217         else
218             for ($i = $ylim - 1; $i >= $yoff; $i--)
219                 $ymatches[$this->yv[$i]][] = $i;
220
221         $this->lcs = 0;
222         $this->seq[0]= $yoff - 1;
223         $this->in_seq = array();
224         $ymids[0] = array();
225     
226         $numer = $xlim - $xoff + $nchunks - 1;
227         $x = $xoff;
228         for ($chunk = 0; $chunk < $nchunks; $chunk++) {
229             if ($chunk > 0)
230                 for ($i = 0; $i <= $this->lcs; $i++)
231                     $ymids[$i][$chunk-1] = $this->seq[$i];
232
233             $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
234             for ( ; $x < $x1; $x++) {
235                 $line = $flip ? $this->yv[$x] : $this->xv[$x];
236                 if (empty($ymatches[$line]))
237                     continue;
238                 $matches = $ymatches[$line];
239                 reset($matches);
240                 while (list ($junk, $y) = each($matches))
241                     if (empty($this->in_seq[$y])) {
242                         $k = $this->_lcs_pos($y);
243                         USE_ASSERTS && assert($k > 0);
244                         $ymids[$k] = $ymids[$k-1];
245                         break;
246                     }
247                 while (list ($junk, $y) = each($matches)) {
248                     if ($y > $this->seq[$k-1]) {
249                         USE_ASSERTS && assert($y < $this->seq[$k]);
250                         // Optimization: this is a common case:
251                         //  next match is just replacing previous match.
252                         $this->in_seq[$this->seq[$k]] = false;
253                         $this->seq[$k] = $y;
254                         $this->in_seq[$y] = 1;
255                     }
256                     else if (empty($this->in_seq[$y])) {
257                         $k = $this->_lcs_pos($y);
258                         USE_ASSERTS && assert($k > 0);
259                         $ymids[$k] = $ymids[$k-1];
260                     }
261                 }
262             }
263         }
264
265         $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
266         $ymid = $ymids[$this->lcs];
267         for ($n = 0; $n < $nchunks - 1; $n++) {
268             $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
269             $y1 = $ymid[$n] + 1;
270             $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
271         }
272         $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
273
274         return array($this->lcs, $seps);
275     }
276
277     function _lcs_pos ($ypos) {
278         $end = $this->lcs;
279         if ($end == 0 || $ypos > $this->seq[$end]) {
280             $this->seq[++$this->lcs] = $ypos;
281             $this->in_seq[$ypos] = 1;
282             return $this->lcs;
283         }
284
285         $beg = 1;
286         while ($beg < $end) {
287             $mid = (int)(($beg + $end) / 2);
288             if ( $ypos > $this->seq[$mid] )
289                 $beg = $mid + 1;
290             else
291                 $end = $mid;
292         }
293
294         USE_ASSERTS && assert($ypos != $this->seq[$end]);
295
296         $this->in_seq[$this->seq[$end]] = false;
297         $this->seq[$end] = $ypos;
298         $this->in_seq[$ypos] = 1;
299         return $end;
300     }
301
302     /* Find LCS of two sequences.
303      *
304      * The results are recorded in the vectors $this->{x,y}changed[], by
305      * storing a 1 in the element for each line that is an insertion
306      * or deletion (ie. is not in the LCS).
307      *
308      * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
309      *
310      * Note that XLIM, YLIM are exclusive bounds.
311      * All line numbers are origin-0 and discarded lines are not counted.
312      */
313     function _compareseq ($xoff, $xlim, $yoff, $ylim) {
314         // Slide down the bottom initial diagonal.
315         while ($xoff < $xlim && $yoff < $ylim
316                && $this->xv[$xoff] == $this->yv[$yoff]) {
317             ++$xoff;
318             ++$yoff;
319         }
320
321         // Slide up the top initial diagonal.
322         while ($xlim > $xoff && $ylim > $yoff
323                && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
324             --$xlim;
325             --$ylim;
326         }
327
328         if ($xoff == $xlim || $yoff == $ylim)
329             $lcs = 0;
330         else {
331             // This is ad hoc but seems to work well.
332             //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
333             //$nchunks = max(2,min(8,(int)$nchunks));
334             $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
335             list ($lcs, $seps)
336                 = $this->_diag($xoff,$xlim,$yoff, $ylim,$nchunks);
337         }
338
339         if ($lcs == 0) {
340             // X and Y sequences have no common subsequence:
341             // mark all changed.
342             while ($yoff < $ylim)
343                 $this->ychanged[$this->yind[$yoff++]] = 1;
344             while ($xoff < $xlim)
345                 $this->xchanged[$this->xind[$xoff++]] = 1;
346         }
347         else {
348             // Use the partitions to split this problem into subproblems.
349             reset($seps);
350             $pt1 = $seps[0];
351             while ($pt2 = next($seps)) {
352                 $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
353                 $pt1 = $pt2;
354             }
355         }
356     }
357
358     /* Adjust inserts/deletes of identical lines to join changes
359      * as much as possible.
360      *
361      * We do something when a run of changed lines include a
362      * line at one end and has an excluded, identical line at the other.
363      * We are free to choose which identical line is included.
364      * `compareseq' usually chooses the one at the beginning,
365      * but usually it is cleaner to consider the following identical line
366      * to be the "change".
367      *
368      * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
369      */
370     function _shift_boundaries ($lines, &$changed, $other_changed) {
371         $i = 0;
372         $j = 0;
373
374         USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
375         $len = sizeof($lines);
376         $other_len = sizeof($other_changed);
377
378         while (1) {
379             /*
380              * Scan forwards to find beginning of another run of changes.
381              * Also keep track of the corresponding point in the other file.
382              *
383              * Throughout this code, $i and $j are adjusted together so that
384              * the first $i elements of $changed and the first $j elements
385              * of $other_changed both contain the same number of zeros
386              * (unchanged lines).
387              * Furthermore, $j is always kept so that $j == $other_len or
388              * $other_changed[$j] == false.
389              */
390             while ($j < $other_len && $other_changed[$j])
391                 $j++;
392             
393             while ($i < $len && ! $changed[$i]) {
394                 USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
395                 $i++; $j++;
396                 while ($j < $other_len && $other_changed[$j])
397                     $j++;
398             }
399             
400             if ($i == $len)
401                 break;
402
403             $start = $i;
404
405             // Find the end of this run of changes.
406             while (++$i < $len && $changed[$i])
407                 continue;
408
409             do {
410                 /*
411                  * Record the length of this run of changes, so that
412                  * we can later determine whether the run has grown.
413                  */
414                 $runlength = $i - $start;
415
416                 /*
417                  * Move the changed region back, so long as the
418                  * previous unchanged line matches the last changed one.
419                  * This merges with previous changed regions.
420                  */
421                 while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
422                     $changed[--$start] = 1;
423                     $changed[--$i] = false;
424                     while ($start > 0 && $changed[$start - 1])
425                         $start--;
426                     USE_ASSERTS && assert('$j > 0');
427                     while ($other_changed[--$j])
428                         continue;
429                     USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
430                 }
431
432                 /*
433                  * Set CORRESPONDING to the end of the changed run, at the last
434                  * point where it corresponds to a changed run in the other file.
435                  * CORRESPONDING == LEN means no such point has been found.
436                  */
437                 $corresponding = $j < $other_len ? $i : $len;
438
439                 /*
440                  * Move the changed region forward, so long as the
441                  * first changed line matches the following unchanged one.
442                  * This merges with following changed regions.
443                  * Do this second, so that if there are no merges,
444                  * the changed region is moved forward as far as possible.
445                  */
446                 while ($i < $len && $lines[$start] == $lines[$i]) {
447                     $changed[$start++] = false;
448                     $changed[$i++] = 1;
449                     while ($i < $len && $changed[$i])
450                         $i++;
451
452                     USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
453                     $j++;
454                     if ($j < $other_len && $other_changed[$j]) {
455                         $corresponding = $i;
456                         while ($j < $other_len && $other_changed[$j])
457                             $j++;
458                     }
459                 }
460             } while ($runlength != $i - $start);
461
462             /*
463              * If possible, move the fully-merged run of changes
464              * back to a corresponding run in the other file.
465              */
466             while ($corresponding < $i) {
467                 $changed[--$start] = 1;
468                 $changed[--$i] = 0;
469                 USE_ASSERTS && assert('$j > 0');
470                 while ($other_changed[--$j])
471                     continue;
472                 USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
473             }
474         }
475     }
476 }
477
478 /**
479  * Class representing a 'diff' between two sequences of strings.
480  */
481 class Diff 
482 {
483     var $edits;
484
485     /**
486      * Constructor.
487      * Computes diff between sequences of strings.
488      *
489      * @param $from_lines array An array of strings.
490      *        (Typically these are lines from a file.)
491      * @param $to_lines array An array of strings.
492      */
493     function Diff($from_lines, $to_lines) {
494         $eng = new _DiffEngine;
495         $this->edits = $eng->diff($from_lines, $to_lines);
496         //$this->_check($from_lines, $to_lines);
497     }
498
499     /**
500      * Compute reversed Diff.
501      *
502      * SYNOPSIS:
503      *
504      *  $diff = new Diff($lines1, $lines2);
505      *  $rev = $diff->reverse();
506      * @return object A Diff object representing the inverse of the
507      *                original diff.
508      */
509     function reverse () {
510         $rev = $this;
511         $rev->edits = array();
512         foreach ($this->edits as $edit) {
513             $rev->edits[] = $edit->reverse();
514         }
515         return $rev;
516     }
517
518     /**
519      * Check for empty diff.
520      *
521      * @return bool True iff two sequences were identical.
522      */
523     function isEmpty () {
524         foreach ($this->edits as $edit) {
525             if ($edit->type != 'copy')
526                 return false;
527         }
528         return true;
529     }
530   
531     /**
532      * Compute the length of the Longest Common Subsequence (LCS).
533      *
534      * This is mostly for diagnostic purposed.
535      *
536      * @return int The length of the LCS.
537      */
538     function lcs () {
539         $lcs = 0;
540         foreach ($this->edits as $edit) {
541             if ($edit->type == 'copy')
542                 $lcs += sizeof($edit->orig);
543         }
544         return $lcs;
545     }
546
547     /**
548      * Get the original set of lines.
549      *
550      * This reconstructs the $from_lines parameter passed to the
551      * constructor.
552      *
553      * @return array The original sequence of strings.
554      */
555     function orig() {
556         $lines = array();
557         
558         foreach ($this->edits as $edit) {
559             if ($edit->orig)
560                 array_splice($lines, sizeof($lines), 0, $edit->orig);
561         }
562         return $lines;
563     }
564
565     /**
566      * Get the final set of lines.
567      *
568      * This reconstructs the $to_lines parameter passed to the
569      * constructor.
570      *
571      * @return array The sequence of strings.
572      */
573     function final() {
574         $lines = array();
575         
576         foreach ($this->edits as $edit) {
577             if ($edit->final)
578                 array_splice($lines, sizeof($lines), 0, $edit->final);
579         }
580         return $lines;
581     }
582         
583     /**
584      * Check a Diff for validity. 
585      *
586      * This is here only for debugging purposes.
587      */
588     function _check ($from_lines, $to_lines) {
589         if (serialize($from_lines) != serialize($this->orig()))
590             trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
591         if (serialize($to_lines) != serialize($this->final()))
592             trigger_error("Reconstructed final doesn't match", E_USER_ERROR);
593
594         $rev = $this->reverse();
595         if (serialize($to_lines) != serialize($rev->orig()))
596             trigger_error("Reversed original doesn't match", E_USER_ERROR);
597         if (serialize($from_lines) != serialize($rev->final()))
598             trigger_error("Reversed final doesn't match", E_USER_ERROR);
599
600
601         $prevtype = 'none';
602         foreach ($this->edits as $edit) {
603             if ( $prevtype == $edit->type )
604                 trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
605             $prevtype = $edit->type;
606         }
607
608         $lcs = $this->lcs();
609         trigger_error("Diff okay: LCS = $lcs", E_USER_NOTICE);
610     }
611 }
612
613
614 if (!function_exists('array_map')) {
615     function array_map($map_func, $array) {
616         $result = array();
617         foreach ($array as $x)
618             $result[] = call_user_func($map_func, $x);
619         return $result;
620     }
621 }
622
623             
624 /**
625  * FIXME: bad name.
626  */
627 class MappedDiff
628 extends Diff
629 {
630     /**
631      * Constructor.
632      *
633      * Computes diff between sequences of strings.
634      *
635      * This can be used to compute things like
636      * case-insensitve diffs, or diffs which ignore
637      * changes in white-space.
638      *
639      * @param $from_lines array An array of strings.
640      *  (Typically these are lines from a file.)
641      *
642      * @param $to_lines array An array of strings.
643      *
644      * @param $mapped_from_lines array This array should
645      *  have the same size number of elements as $from_lines.
646      *  The elements in $mapped_from_lines and
647      *  $mapped_to_lines are what is actually compared
648      *  when computing the diff.
649      *
650      * @param $mapped_to_lines array This array should
651      *  have the same number of elements as $to_lines.
652      */
653     function MappedDiff($from_lines, $to_lines,
654                         $mapped_from_lines, $mapped_to_lines) {
655
656         assert(sizeof($from_lines) == sizeof($mapped_from_lines));
657         assert(sizeof($to_lines) == sizeof($mapped_to_lines));
658         
659         $this->Diff($mapped_from_lines, $mapped_to_lines);
660
661         $xi = $yi = 0;
662         for ($i = 0; $i < sizeof($this->edits); $i++) {
663             $orig = &$this->edits[$i]->orig;
664             if (is_array($orig)) {
665                 $orig = array_slice($from_lines, $xi, sizeof($orig));
666                 $xi += sizeof($orig);
667             }
668             
669             $final = &$this->edits[$i]->final;
670             if (is_array($final)) {
671                 $final = array_slice($to_lines, $yi, sizeof($final));
672                 $yi += sizeof($final);
673             }
674         }
675     }
676 }
677
678
679 /**
680  * A class to format Diffs
681  *
682  * This class formats the diff in classic diff format.
683  * It is intended that this class be customized via inheritance,
684  * to obtain fancier outputs.
685  */
686 class DiffFormatter
687 {
688     /**
689      * Number of leading context "lines" to preserve.
690      *
691      * This should be left at zero for this class, but subclasses
692      * may want to set this to other values.
693      */
694     var $leading_context_lines = 0;
695
696     /**
697      * Number of trailing context "lines" to preserve.
698      *
699      * This should be left at zero for this class, but subclasses
700      * may want to set this to other values.
701      */
702     var $trailing_context_lines = 0;
703
704     /**
705      * Format a diff.
706      *
707      * @param $diff object A Diff object.
708      * @return string The formatted output.
709      */
710     function format($diff) {
711
712         $xi = $yi = 1;
713         $block = false;
714         $context = array();
715
716         $nlead = $this->leading_context_lines;
717         $ntrail = $this->trailing_context_lines;
718
719         ob_start();
720         $this->_start_diff();
721
722         foreach ($diff->edits as $edit) {
723             if ($edit->type == 'copy') {
724                 if (is_array($block)) {
725                     if (sizeof($edit->orig) <= $nlead + $ntrail) {
726                         $block[] = $edit;
727                     }
728                     else{
729                         if ($ntrail) {
730                             $context = array_slice($edit->orig, 0, $ntrail);
731                             $block[] = new _DiffOp_Copy($context);
732                         }
733                         $this->_block($x0, $ntrail + $xi - $x0,
734                                       $y0, $ntrail + $yi - $y0,
735                                       $block);
736                         $block = false;
737                     }
738                 }
739                 $context = $edit->orig;
740             }
741             else {
742                 if (! is_array($block)) {
743                     $context = array_slice($context, sizeof($context) - $nlead);
744                     $x0 = $xi - sizeof($context);
745                     $y0 = $yi - sizeof($context);
746                     $block = array();
747                     if ($context)
748                         $block[] = new _DiffOp_Copy($context);
749                 }
750                 $block[] = $edit;
751             }
752
753             if ($edit->orig)
754                 $xi += sizeof($edit->orig);
755             if ($edit->final)
756                 $yi += sizeof($edit->final);
757         }
758
759         if (is_array($block))
760             $this->_block($x0, $xi - $x0,
761                           $y0, $yi - $y0,
762                           $block);
763
764         $this->_end_diff();
765
766         $val = ob_get_contents();
767         ob_end_clean();
768         return $val;
769     }
770
771     function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
772         $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
773         foreach ($edits as $edit) {
774             if ($edit->type == 'copy')
775                 $this->_context($edit->orig);
776             elseif ($edit->type == 'add')
777                 $this->_added($edit->final);
778             elseif ($edit->type == 'delete')
779                 $this->_deleted($edit->orig);
780             elseif ($edit->type == 'change')
781                 $this->_changed($edit->orig, $edit->final);
782             else
783                 trigger_error("Unknown edit type", E_USER_ERROR);
784         }
785         $this->_end_block();
786     }
787
788     function _start_diff() {
789     }
790
791     function _end_diff() {
792     }
793
794     function _block_header($xbeg, $xlen, $ybeg, $ylen) {
795         if ($xlen > 1)
796             $xbeg .= "," . ($xbeg + $xlen - 1);
797         if ($ylen > 1)
798             $ybeg .= "," . ($ybeg + $ylen - 1);
799
800         return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
801     }
802     
803     function _start_block($header) {
804         echo $header;
805     }
806     
807     function _end_block() {
808     }
809
810     function _lines($lines, $prefix = ' ') {
811         foreach ($lines as $line)
812             echo "$prefix $line\n";
813     }
814     
815     function _context($lines) {
816         $this->_lines($lines);
817     }
818
819     function _added($lines) {
820         $this->_lines($lines, ">");
821     }
822     function _deleted($lines) {
823         $this->_lines($lines, "<");
824     }
825
826     function _changed($orig, $final) {
827         $this->_deleted($orig);
828         echo "---\n";
829         $this->_added($final);
830     }
831 }
832
833 /**
834  * "Unified" diff formatter.
835  *
836  * This class formats the diff in classic "unified diff" format.
837  */
838 class UnifiedDiffFormatter extends DiffFormatter
839 {
840     function UnifiedDiffFormatter($context_lines = 4) {
841         $this->leading_context_lines = $context_lines;
842         $this->trailing_context_lines = $context_lines;
843     }
844     
845     function _block_header($xbeg, $xlen, $ybeg, $ylen) {
846         if ($xlen != 1)
847             $xbeg .= "," . $xlen;
848         if ($ylen != 1)
849             $ybeg .= "," . $ylen;
850         return "@@ -$xbeg +$ybeg @@";
851     }
852
853     function _added($lines) {
854         $this->_lines($lines, "+");
855     }
856     function _deleted($lines) {
857         $this->_lines($lines, "-");
858     }
859     function _changed($orig, $final) {
860         $this->_deleted($orig);
861         $this->_added($final);
862     }
863 }
864
865 // Local Variables:
866 // mode: php
867 // tab-width: 8
868 // c-basic-offset: 4
869 // c-hanging-comment-ender-p: nil
870 // indent-tabs-mode: nil
871 // End:   
872 ?>