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