1<?php 2/** 3 * A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3) 4 * 5 * Additions by Axel Boldt for MediaWiki 6 * 7 * @copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org> 8 * @license You may copy this code freely under the conditions of the GPL. 9 */ 10define('USE_ASSERTS', function_exists('assert')); 11 12class _DiffOp { 13 var $type; 14 var $orig; 15 var $closing; 16 17 /** 18 * @return _DiffOp 19 */ 20 function reverse() { 21 trigger_error("pure virtual", E_USER_ERROR); 22 } 23 24 function norig() { 25 return $this->orig ? count($this->orig) : 0; 26 } 27 28 function nclosing() { 29 return $this->closing ? count($this->closing) : 0; 30 } 31} 32 33class _DiffOp_Copy extends _DiffOp { 34 var $type = 'copy'; 35 36 function __construct($orig, $closing = false) { 37 if (!is_array($closing)) 38 $closing = $orig; 39 $this->orig = $orig; 40 $this->closing = $closing; 41 } 42 43 function reverse() { 44 return new _DiffOp_Copy($this->closing, $this->orig); 45 } 46} 47 48class _DiffOp_Delete extends _DiffOp { 49 var $type = 'delete'; 50 51 function __construct($lines) { 52 $this->orig = $lines; 53 $this->closing = false; 54 } 55 56 function reverse() { 57 return new _DiffOp_Add($this->orig); 58 } 59} 60 61class _DiffOp_Add extends _DiffOp { 62 var $type = 'add'; 63 64 function __construct($lines) { 65 $this->closing = $lines; 66 $this->orig = false; 67 } 68 69 function reverse() { 70 return new _DiffOp_Delete($this->closing); 71 } 72} 73 74class _DiffOp_Change extends _DiffOp { 75 var $type = 'change'; 76 77 function __construct($orig, $closing) { 78 $this->orig = $orig; 79 $this->closing = $closing; 80 } 81 82 function reverse() { 83 return new _DiffOp_Change($this->closing, $this->orig); 84 } 85} 86 87 88/** 89 * Class used internally by Diff to actually compute the diffs. 90 * 91 * The algorithm used here is mostly lifted from the perl module 92 * Algorithm::Diff (version 1.06) by Ned Konz, which is available at: 93 * http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip 94 * 95 * More ideas are taken from: 96 * http://www.ics.uci.edu/~eppstein/161/960229.html 97 * 98 * Some ideas are (and a bit of code) are from from analyze.c, from GNU 99 * diffutils-2.7, which can be found at: 100 * ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz 101 * 102 * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations) 103 * are my own. 104 * 105 * @author Geoffrey T. Dairiki 106 * @access private 107 */ 108class _DiffEngine { 109 110 var $xchanged = array(); 111 var $ychanged = array(); 112 var $xv = array(); 113 var $yv = array(); 114 var $xind = array(); 115 var $yind = array(); 116 var $seq; 117 var $in_seq; 118 var $lcs; 119 120 /** 121 * @param array $from_lines 122 * @param array $to_lines 123 * @return _DiffOp[] 124 */ 125 function diff($from_lines, $to_lines) { 126 $n_from = count($from_lines); 127 $n_to = count($to_lines); 128 129 $this->xchanged = $this->ychanged = array(); 130 $this->xv = $this->yv = array(); 131 $this->xind = $this->yind = array(); 132 unset($this->seq); 133 unset($this->in_seq); 134 unset($this->lcs); 135 136 // Skip leading common lines. 137 for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) { 138 if ($from_lines[$skip] != $to_lines[$skip]) 139 break; 140 $this->xchanged[$skip] = $this->ychanged[$skip] = false; 141 } 142 // Skip trailing common lines. 143 $xi = $n_from; 144 $yi = $n_to; 145 for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) { 146 if ($from_lines[$xi] != $to_lines[$yi]) 147 break; 148 $this->xchanged[$xi] = $this->ychanged[$yi] = false; 149 } 150 151 // Ignore lines which do not exist in both files. 152 for ($xi = $skip; $xi < $n_from - $endskip; $xi++) 153 $xhash[$from_lines[$xi]] = 1; 154 for ($yi = $skip; $yi < $n_to - $endskip; $yi++) { 155 $line = $to_lines[$yi]; 156 if (($this->ychanged[$yi] = empty($xhash[$line]))) 157 continue; 158 $yhash[$line] = 1; 159 $this->yv[] = $line; 160 $this->yind[] = $yi; 161 } 162 for ($xi = $skip; $xi < $n_from - $endskip; $xi++) { 163 $line = $from_lines[$xi]; 164 if (($this->xchanged[$xi] = empty($yhash[$line]))) 165 continue; 166 $this->xv[] = $line; 167 $this->xind[] = $xi; 168 } 169 170 // Find the LCS. 171 $this->_compareseq(0, count($this->xv), 0, count($this->yv)); 172 173 // Merge edits when possible 174 $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged); 175 $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged); 176 177 // Compute the edit operations. 178 $edits = array(); 179 $xi = $yi = 0; 180 while ($xi < $n_from || $yi < $n_to) { 181 USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]); 182 USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]); 183 184 // Skip matching "snake". 185 $copy = array(); 186 while ($xi < $n_from && $yi < $n_to && !$this->xchanged[$xi] && !$this->ychanged[$yi]) { 187 $copy[] = $from_lines[$xi++]; 188 ++$yi; 189 } 190 if ($copy) 191 $edits[] = new _DiffOp_Copy($copy); 192 193 // Find deletes & adds. 194 $delete = array(); 195 while ($xi < $n_from && $this->xchanged[$xi]) 196 $delete[] = $from_lines[$xi++]; 197 198 $add = array(); 199 while ($yi < $n_to && $this->ychanged[$yi]) 200 $add[] = $to_lines[$yi++]; 201 202 if ($delete && $add) 203 $edits[] = new _DiffOp_Change($delete, $add); 204 elseif ($delete) 205 $edits[] = new _DiffOp_Delete($delete); 206 elseif ($add) 207 $edits[] = new _DiffOp_Add($add); 208 } 209 return $edits; 210 } 211 212 213 /** 214 * Divide the Largest Common Subsequence (LCS) of the sequences 215 * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally 216 * sized segments. 217 * 218 * Returns (LCS, PTS). LCS is the length of the LCS. PTS is an 219 * array of NCHUNKS+1 (X, Y) indexes giving the diving points between 220 * sub sequences. The first sub-sequence is contained in [X0, X1), 221 * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on. Note 222 * that (X0, Y0) == (XOFF, YOFF) and 223 * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM). 224 * 225 * This function assumes that the first lines of the specified portions 226 * of the two files do not match, and likewise that the last lines do not 227 * match. The caller must trim matching lines from the beginning and end 228 * of the portions it is going to specify. 229 * 230 * @param integer $xoff 231 * @param integer $xlim 232 * @param integer $yoff 233 * @param integer $ylim 234 * @param integer $nchunks 235 * 236 * @return array 237 */ 238 function _diag($xoff, $xlim, $yoff, $ylim, $nchunks) { 239 $flip = false; 240 241 if ($xlim - $xoff > $ylim - $yoff) { 242 // Things seems faster (I'm not sure I understand why) 243 // when the shortest sequence in X. 244 $flip = true; 245 list ($xoff, $xlim, $yoff, $ylim) = array($yoff, $ylim, $xoff, $xlim); 246 } 247 248 if ($flip) 249 for ($i = $ylim - 1; $i >= $yoff; $i--) 250 $ymatches[$this->xv[$i]][] = $i; 251 else 252 for ($i = $ylim - 1; $i >= $yoff; $i--) 253 $ymatches[$this->yv[$i]][] = $i; 254 255 $this->lcs = 0; 256 $this->seq[0]= $yoff - 1; 257 $this->in_seq = array(); 258 $ymids[0] = array(); 259 260 $numer = $xlim - $xoff + $nchunks - 1; 261 $x = $xoff; 262 for ($chunk = 0; $chunk < $nchunks; $chunk++) { 263 if ($chunk > 0) 264 for ($i = 0; $i <= $this->lcs; $i++) 265 $ymids[$i][$chunk-1] = $this->seq[$i]; 266 267 $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks); 268 for ( ; $x < $x1; $x++) { 269 $line = $flip ? $this->yv[$x] : $this->xv[$x]; 270 if (empty($ymatches[$line])) 271 continue; 272 $matches = $ymatches[$line]; 273 reset($matches); 274 while (list ($junk, $y) = each($matches)) 275 if (empty($this->in_seq[$y])) { 276 $k = $this->_lcs_pos($y); 277 USE_ASSERTS && assert($k > 0); 278 $ymids[$k] = $ymids[$k-1]; 279 break; 280 } 281 while (list ($junk, $y) = each($matches)) { 282 if ($y > $this->seq[$k-1]) { 283 USE_ASSERTS && assert($y < $this->seq[$k]); 284 // Optimization: this is a common case: 285 // next match is just replacing previous match. 286 $this->in_seq[$this->seq[$k]] = false; 287 $this->seq[$k] = $y; 288 $this->in_seq[$y] = 1; 289 } 290 else if (empty($this->in_seq[$y])) { 291 $k = $this->_lcs_pos($y); 292 USE_ASSERTS && assert($k > 0); 293 $ymids[$k] = $ymids[$k-1]; 294 } 295 } 296 } 297 } 298 299 $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff); 300 $ymid = $ymids[$this->lcs]; 301 for ($n = 0; $n < $nchunks - 1; $n++) { 302 $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks); 303 $y1 = $ymid[$n] + 1; 304 $seps[] = $flip ? array($y1, $x1) : array($x1, $y1); 305 } 306 $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim); 307 308 return array($this->lcs, $seps); 309 } 310 311 function _lcs_pos($ypos) { 312 $end = $this->lcs; 313 if ($end == 0 || $ypos > $this->seq[$end]) { 314 $this->seq[++$this->lcs] = $ypos; 315 $this->in_seq[$ypos] = 1; 316 return $this->lcs; 317 } 318 319 $beg = 1; 320 while ($beg < $end) { 321 $mid = (int)(($beg + $end) / 2); 322 if ($ypos > $this->seq[$mid]) 323 $beg = $mid + 1; 324 else 325 $end = $mid; 326 } 327 328 USE_ASSERTS && assert($ypos != $this->seq[$end]); 329 330 $this->in_seq[$this->seq[$end]] = false; 331 $this->seq[$end] = $ypos; 332 $this->in_seq[$ypos] = 1; 333 return $end; 334 } 335 336 /** 337 * Find LCS of two sequences. 338 * 339 * The results are recorded in the vectors $this->{x,y}changed[], by 340 * storing a 1 in the element for each line that is an insertion 341 * or deletion (ie. is not in the LCS). 342 * 343 * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1. 344 * 345 * Note that XLIM, YLIM are exclusive bounds. 346 * All line numbers are origin-0 and discarded lines are not counted. 347 * 348 * @param integer $xoff 349 * @param integer $xlim 350 * @param integer $yoff 351 * @param integer $ylim 352 */ 353 function _compareseq($xoff, $xlim, $yoff, $ylim) { 354 // Slide down the bottom initial diagonal. 355 while ($xoff < $xlim && $yoff < $ylim && $this->xv[$xoff] == $this->yv[$yoff]) { 356 ++$xoff; 357 ++$yoff; 358 } 359 360 // Slide up the top initial diagonal. 361 while ($xlim > $xoff && $ylim > $yoff && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) { 362 --$xlim; 363 --$ylim; 364 } 365 366 if ($xoff == $xlim || $yoff == $ylim) 367 $lcs = 0; 368 else { 369 // This is ad hoc but seems to work well. 370 //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5); 371 //$nchunks = max(2,min(8,(int)$nchunks)); 372 $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1; 373 list ($lcs, $seps) 374 = $this->_diag($xoff,$xlim,$yoff, $ylim,$nchunks); 375 } 376 377 if ($lcs == 0) { 378 // X and Y sequences have no common subsequence: 379 // mark all changed. 380 while ($yoff < $ylim) 381 $this->ychanged[$this->yind[$yoff++]] = 1; 382 while ($xoff < $xlim) 383 $this->xchanged[$this->xind[$xoff++]] = 1; 384 } 385 else { 386 // Use the partitions to split this problem into subproblems. 387 reset($seps); 388 $pt1 = $seps[0]; 389 while ($pt2 = next($seps)) { 390 $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]); 391 $pt1 = $pt2; 392 } 393 } 394 } 395 396 /** 397 * Adjust inserts/deletes of identical lines to join changes 398 * as much as possible. 399 * 400 * We do something when a run of changed lines include a 401 * line at one end and has an excluded, identical line at the other. 402 * We are free to choose which identical line is included. 403 * `compareseq' usually chooses the one at the beginning, 404 * but usually it is cleaner to consider the following identical line 405 * to be the "change". 406 * 407 * This is extracted verbatim from analyze.c (GNU diffutils-2.7). 408 * 409 * @param array $lines 410 * @param array $changed 411 * @param array $other_changed 412 */ 413 function _shift_boundaries($lines, &$changed, $other_changed) { 414 $i = 0; 415 $j = 0; 416 417 USE_ASSERTS && assert('count($lines) == count($changed)'); 418 $len = count($lines); 419 $other_len = count($other_changed); 420 421 while (1) { 422 /* 423 * Scan forwards to find beginning of another run of changes. 424 * Also keep track of the corresponding point in the other file. 425 * 426 * Throughout this code, $i and $j are adjusted together so that 427 * the first $i elements of $changed and the first $j elements 428 * of $other_changed both contain the same number of zeros 429 * (unchanged lines). 430 * Furthermore, $j is always kept so that $j == $other_len or 431 * $other_changed[$j] == false. 432 */ 433 while ($j < $other_len && $other_changed[$j]) 434 $j++; 435 436 while ($i < $len && ! $changed[$i]) { 437 USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]'); 438 $i++; 439 $j++; 440 while ($j < $other_len && $other_changed[$j]) 441 $j++; 442 } 443 444 if ($i == $len) 445 break; 446 447 $start = $i; 448 449 // Find the end of this run of changes. 450 while (++$i < $len && $changed[$i]) 451 continue; 452 453 do { 454 /* 455 * Record the length of this run of changes, so that 456 * we can later determine whether the run has grown. 457 */ 458 $runlength = $i - $start; 459 460 /* 461 * Move the changed region back, so long as the 462 * previous unchanged line matches the last changed one. 463 * This merges with previous changed regions. 464 */ 465 while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) { 466 $changed[--$start] = 1; 467 $changed[--$i] = false; 468 while ($start > 0 && $changed[$start - 1]) 469 $start--; 470 USE_ASSERTS && assert('$j > 0'); 471 while ($other_changed[--$j]) 472 continue; 473 USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]'); 474 } 475 476 /* 477 * Set CORRESPONDING to the end of the changed run, at the last 478 * point where it corresponds to a changed run in the other file. 479 * CORRESPONDING == LEN means no such point has been found. 480 */ 481 $corresponding = $j < $other_len ? $i : $len; 482 483 /* 484 * Move the changed region forward, so long as the 485 * first changed line matches the following unchanged one. 486 * This merges with following changed regions. 487 * Do this second, so that if there are no merges, 488 * the changed region is moved forward as far as possible. 489 */ 490 while ($i < $len && $lines[$start] == $lines[$i]) { 491 $changed[$start++] = false; 492 $changed[$i++] = 1; 493 while ($i < $len && $changed[$i]) 494 $i++; 495 496 USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]'); 497 $j++; 498 if ($j < $other_len && $other_changed[$j]) { 499 $corresponding = $i; 500 while ($j < $other_len && $other_changed[$j]) 501 $j++; 502 } 503 } 504 } while ($runlength != $i - $start); 505 506 /* 507 * If possible, move the fully-merged run of changes 508 * back to a corresponding run in the other file. 509 */ 510 while ($corresponding < $i) { 511 $changed[--$start] = 1; 512 $changed[--$i] = 0; 513 USE_ASSERTS && assert('$j > 0'); 514 while ($other_changed[--$j]) 515 continue; 516 USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]'); 517 } 518 } 519 } 520} 521 522/** 523 * Class representing a 'diff' between two sequences of strings. 524 */ 525class Diff { 526 527 var $edits; 528 529 /** 530 * Constructor. 531 * Computes diff between sequences of strings. 532 * 533 * @param array $from_lines An array of strings. 534 * (Typically these are lines from a file.) 535 * @param array $to_lines An array of strings. 536 */ 537 function __construct($from_lines, $to_lines) { 538 $eng = new _DiffEngine; 539 $this->edits = $eng->diff($from_lines, $to_lines); 540 //$this->_check($from_lines, $to_lines); 541 } 542 543 /** 544 * Compute reversed Diff. 545 * 546 * SYNOPSIS: 547 * 548 * $diff = new Diff($lines1, $lines2); 549 * $rev = $diff->reverse(); 550 * 551 * @return Diff A Diff object representing the inverse of the 552 * original diff. 553 */ 554 function reverse() { 555 $rev = $this; 556 $rev->edits = array(); 557 foreach ($this->edits as $edit) { 558 $rev->edits[] = $edit->reverse(); 559 } 560 return $rev; 561 } 562 563 /** 564 * Check for empty diff. 565 * 566 * @return bool True iff two sequences were identical. 567 */ 568 function isEmpty() { 569 foreach ($this->edits as $edit) { 570 if ($edit->type != 'copy') 571 return false; 572 } 573 return true; 574 } 575 576 /** 577 * Compute the length of the Longest Common Subsequence (LCS). 578 * 579 * This is mostly for diagnostic purposed. 580 * 581 * @return int The length of the LCS. 582 */ 583 function lcs() { 584 $lcs = 0; 585 foreach ($this->edits as $edit) { 586 if ($edit->type == 'copy') 587 $lcs += count($edit->orig); 588 } 589 return $lcs; 590 } 591 592 /** 593 * Get the original set of lines. 594 * 595 * This reconstructs the $from_lines parameter passed to the 596 * constructor. 597 * 598 * @return array The original sequence of strings. 599 */ 600 function orig() { 601 $lines = array(); 602 603 foreach ($this->edits as $edit) { 604 if ($edit->orig) 605 array_splice($lines, count($lines), 0, $edit->orig); 606 } 607 return $lines; 608 } 609 610 /** 611 * Get the closing set of lines. 612 * 613 * This reconstructs the $to_lines parameter passed to the 614 * constructor. 615 * 616 * @return array The sequence of strings. 617 */ 618 function closing() { 619 $lines = array(); 620 621 foreach ($this->edits as $edit) { 622 if ($edit->closing) 623 array_splice($lines, count($lines), 0, $edit->closing); 624 } 625 return $lines; 626 } 627 628 /** 629 * Check a Diff for validity. 630 * 631 * This is here only for debugging purposes. 632 * 633 * @param mixed $from_lines 634 * @param mixed $to_lines 635 */ 636 function _check($from_lines, $to_lines) { 637 if (serialize($from_lines) != serialize($this->orig())) 638 trigger_error("Reconstructed original doesn't match", E_USER_ERROR); 639 if (serialize($to_lines) != serialize($this->closing())) 640 trigger_error("Reconstructed closing doesn't match", E_USER_ERROR); 641 642 $rev = $this->reverse(); 643 if (serialize($to_lines) != serialize($rev->orig())) 644 trigger_error("Reversed original doesn't match", E_USER_ERROR); 645 if (serialize($from_lines) != serialize($rev->closing())) 646 trigger_error("Reversed closing doesn't match", E_USER_ERROR); 647 648 $prevtype = 'none'; 649 foreach ($this->edits as $edit) { 650 if ($prevtype == $edit->type) 651 trigger_error("Edit sequence is non-optimal", E_USER_ERROR); 652 $prevtype = $edit->type; 653 } 654 655 $lcs = $this->lcs(); 656 trigger_error("Diff okay: LCS = $lcs", E_USER_NOTICE); 657 } 658} 659 660/** 661 * FIXME: bad name. 662 */ 663class MappedDiff extends Diff { 664 /** 665 * Constructor. 666 * 667 * Computes diff between sequences of strings. 668 * 669 * This can be used to compute things like 670 * case-insensitve diffs, or diffs which ignore 671 * changes in white-space. 672 * 673 * @param string[] $from_lines An array of strings. 674 * (Typically these are lines from a file.) 675 * 676 * @param string[] $to_lines An array of strings. 677 * 678 * @param string[] $mapped_from_lines This array should 679 * have the same size number of elements as $from_lines. 680 * The elements in $mapped_from_lines and 681 * $mapped_to_lines are what is actually compared 682 * when computing the diff. 683 * 684 * @param string[] $mapped_to_lines This array should 685 * have the same number of elements as $to_lines. 686 */ 687 function __construct($from_lines, $to_lines, $mapped_from_lines, $mapped_to_lines) { 688 689 assert(count($from_lines) == count($mapped_from_lines)); 690 assert(count($to_lines) == count($mapped_to_lines)); 691 692 parent::__construct($mapped_from_lines, $mapped_to_lines); 693 694 $xi = $yi = 0; 695 $ecnt = count($this->edits); 696 for ($i = 0; $i < $ecnt; $i++) { 697 $orig = &$this->edits[$i]->orig; 698 if (is_array($orig)) { 699 $orig = array_slice($from_lines, $xi, count($orig)); 700 $xi += count($orig); 701 } 702 703 $closing = &$this->edits[$i]->closing; 704 if (is_array($closing)) { 705 $closing = array_slice($to_lines, $yi, count($closing)); 706 $yi += count($closing); 707 } 708 } 709 } 710} 711 712/** 713 * A class to format Diffs 714 * 715 * This class formats the diff in classic diff format. 716 * It is intended that this class be customized via inheritance, 717 * to obtain fancier outputs. 718 */ 719class DiffFormatter { 720 /** 721 * Number of leading context "lines" to preserve. 722 * 723 * This should be left at zero for this class, but subclasses 724 * may want to set this to other values. 725 */ 726 var $leading_context_lines = 0; 727 728 /** 729 * Number of trailing context "lines" to preserve. 730 * 731 * This should be left at zero for this class, but subclasses 732 * may want to set this to other values. 733 */ 734 var $trailing_context_lines = 0; 735 736 /** 737 * Format a diff. 738 * 739 * @param Diff $diff A Diff object. 740 * @return string The formatted output. 741 */ 742 function format($diff) { 743 744 $xi = $yi = 1; 745 $x0 = $y0 = 0; 746 $block = false; 747 $context = array(); 748 749 $nlead = $this->leading_context_lines; 750 $ntrail = $this->trailing_context_lines; 751 752 $this->_start_diff(); 753 754 foreach ($diff->edits as $edit) { 755 if ($edit->type == 'copy') { 756 if (is_array($block)) { 757 if (count($edit->orig) <= $nlead + $ntrail) { 758 $block[] = $edit; 759 } 760 else{ 761 if ($ntrail) { 762 $context = array_slice($edit->orig, 0, $ntrail); 763 $block[] = new _DiffOp_Copy($context); 764 } 765 $this->_block($x0, $ntrail + $xi - $x0, $y0, $ntrail + $yi - $y0, $block); 766 $block = false; 767 } 768 } 769 $context = $edit->orig; 770 } 771 else { 772 if (! is_array($block)) { 773 $context = array_slice($context, count($context) - $nlead); 774 $x0 = $xi - count($context); 775 $y0 = $yi - count($context); 776 $block = array(); 777 if ($context) 778 $block[] = new _DiffOp_Copy($context); 779 } 780 $block[] = $edit; 781 } 782 783 if ($edit->orig) 784 $xi += count($edit->orig); 785 if ($edit->closing) 786 $yi += count($edit->closing); 787 } 788 789 if (is_array($block)) 790 $this->_block($x0, $xi - $x0, $y0, $yi - $y0, $block); 791 792 return $this->_end_diff(); 793 } 794 795 /** 796 * @param int $xbeg 797 * @param int $xlen 798 * @param int $ybeg 799 * @param int $ylen 800 * @param array $edits 801 */ 802 function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) { 803 $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen)); 804 foreach ($edits as $edit) { 805 if ($edit->type == 'copy') 806 $this->_context($edit->orig); 807 elseif ($edit->type == 'add') 808 $this->_added($edit->closing); 809 elseif ($edit->type == 'delete') 810 $this->_deleted($edit->orig); 811 elseif ($edit->type == 'change') 812 $this->_changed($edit->orig, $edit->closing); 813 else 814 trigger_error("Unknown edit type", E_USER_ERROR); 815 } 816 $this->_end_block(); 817 } 818 819 function _start_diff() { 820 ob_start(); 821 } 822 823 function _end_diff() { 824 $val = ob_get_contents(); 825 ob_end_clean(); 826 return $val; 827 } 828 829 /** 830 * @param int $xbeg 831 * @param int $xlen 832 * @param int $ybeg 833 * @param int $ylen 834 * @return string 835 */ 836 function _block_header($xbeg, $xlen, $ybeg, $ylen) { 837 if ($xlen > 1) 838 $xbeg .= "," . ($xbeg + $xlen - 1); 839 if ($ylen > 1) 840 $ybeg .= "," . ($ybeg + $ylen - 1); 841 842 return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg; 843 } 844 845 /** 846 * @param string $header 847 */ 848 function _start_block($header) { 849 echo $header; 850 } 851 852 function _end_block() { 853 } 854 855 function _lines($lines, $prefix = ' ') { 856 foreach ($lines as $line) 857 echo "$prefix ".$this->_escape($line)."\n"; 858 } 859 860 function _context($lines) { 861 $this->_lines($lines); 862 } 863 864 function _added($lines) { 865 $this->_lines($lines, ">"); 866 } 867 function _deleted($lines) { 868 $this->_lines($lines, "<"); 869 } 870 871 function _changed($orig, $closing) { 872 $this->_deleted($orig); 873 echo "---\n"; 874 $this->_added($closing); 875 } 876 877 /** 878 * Escape string 879 * 880 * Override this method within other formatters if escaping required. 881 * Base class requires $str to be returned WITHOUT escaping. 882 * 883 * @param $str string Text string to escape 884 * @return string The escaped string. 885 */ 886 function _escape($str){ 887 return $str; 888 } 889} 890 891/** 892 * Utilityclass for styling HTML formatted diffs 893 * 894 * Depends on global var $DIFF_INLINESTYLES, if true some minimal predefined 895 * inline styles are used. Useful for HTML mails and RSS feeds 896 * 897 * @author Andreas Gohr <andi@splitbrain.org> 898 */ 899class HTMLDiff { 900 /** 901 * Holds the style names and basic CSS 902 */ 903 static public $styles = array( 904 'diff-addedline' => 'background-color: #ddffdd;', 905 'diff-deletedline' => 'background-color: #ffdddd;', 906 'diff-context' => 'background-color: #f5f5f5;', 907 'diff-mark' => 'color: #ff0000;', 908 ); 909 910 /** 911 * Return a class or style parameter 912 * 913 * @param string $classname 914 * 915 * @return string 916 */ 917 static function css($classname){ 918 global $DIFF_INLINESTYLES; 919 920 if($DIFF_INLINESTYLES){ 921 if(!isset(self::$styles[$classname])) return ''; 922 return 'style="'.self::$styles[$classname].'"'; 923 }else{ 924 return 'class="'.$classname.'"'; 925 } 926 } 927} 928 929/** 930 * Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3 931 * 932 */ 933 934define('NBSP', "\xC2\xA0"); // utf-8 non-breaking space. 935 936class _HWLDF_WordAccumulator { 937 938 function __construct() { 939 $this->_lines = array(); 940 $this->_line = ''; 941 $this->_group = ''; 942 $this->_tag = ''; 943 } 944 945 function _flushGroup($new_tag) { 946 if ($this->_group !== '') { 947 if ($this->_tag == 'mark') 948 $this->_line .= '<strong '.HTMLDiff::css('diff-mark').'>'.$this->_escape($this->_group).'</strong>'; 949 elseif ($this->_tag == 'add') 950 $this->_line .= '<span '.HTMLDiff::css('diff-addedline').'>'.$this->_escape($this->_group).'</span>'; 951 elseif ($this->_tag == 'del') 952 $this->_line .= '<span '.HTMLDiff::css('diff-deletedline').'><del>'.$this->_escape($this->_group).'</del></span>'; 953 else 954 $this->_line .= $this->_escape($this->_group); 955 } 956 $this->_group = ''; 957 $this->_tag = $new_tag; 958 } 959 960 /** 961 * @param string $new_tag 962 */ 963 function _flushLine($new_tag) { 964 $this->_flushGroup($new_tag); 965 if ($this->_line != '') 966 $this->_lines[] = $this->_line; 967 $this->_line = ''; 968 } 969 970 function addWords($words, $tag = '') { 971 if ($tag != $this->_tag) 972 $this->_flushGroup($tag); 973 974 foreach ($words as $word) { 975 // new-line should only come as first char of word. 976 if ($word == '') 977 continue; 978 if ($word[0] == "\n") { 979 $this->_group .= NBSP; 980 $this->_flushLine($tag); 981 $word = substr($word, 1); 982 } 983 assert(!strstr($word, "\n")); 984 $this->_group .= $word; 985 } 986 } 987 988 function getLines() { 989 $this->_flushLine('~done'); 990 return $this->_lines; 991 } 992 993 function _escape($str){ 994 return hsc($str); 995 } 996} 997 998class WordLevelDiff extends MappedDiff { 999 1000 function __construct($orig_lines, $closing_lines) { 1001 list ($orig_words, $orig_stripped) = $this->_split($orig_lines); 1002 list ($closing_words, $closing_stripped) = $this->_split($closing_lines); 1003 1004 parent::__construct($orig_words, $closing_words, $orig_stripped, $closing_stripped); 1005 } 1006 1007 function _split($lines) { 1008 if (!preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xsu', 1009 implode("\n", $lines), $m)) { 1010 return array(array(''), array('')); 1011 } 1012 return array($m[0], $m[1]); 1013 } 1014 1015 function orig() { 1016 $orig = new _HWLDF_WordAccumulator; 1017 1018 foreach ($this->edits as $edit) { 1019 if ($edit->type == 'copy') 1020 $orig->addWords($edit->orig); 1021 elseif ($edit->orig) 1022 $orig->addWords($edit->orig, 'mark'); 1023 } 1024 return $orig->getLines(); 1025 } 1026 1027 function closing() { 1028 $closing = new _HWLDF_WordAccumulator; 1029 1030 foreach ($this->edits as $edit) { 1031 if ($edit->type == 'copy') 1032 $closing->addWords($edit->closing); 1033 elseif ($edit->closing) 1034 $closing->addWords($edit->closing, 'mark'); 1035 } 1036 return $closing->getLines(); 1037 } 1038} 1039 1040class InlineWordLevelDiff extends MappedDiff { 1041 1042 function __construct($orig_lines, $closing_lines) { 1043 list ($orig_words, $orig_stripped) = $this->_split($orig_lines); 1044 list ($closing_words, $closing_stripped) = $this->_split($closing_lines); 1045 1046 parent::__construct($orig_words, $closing_words, $orig_stripped, $closing_stripped); 1047 } 1048 1049 function _split($lines) { 1050 if (!preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xsu', 1051 implode("\n", $lines), $m)) { 1052 return array(array(''), array('')); 1053 } 1054 return array($m[0], $m[1]); 1055 } 1056 1057 function inline() { 1058 $orig = new _HWLDF_WordAccumulator; 1059 foreach ($this->edits as $edit) { 1060 if ($edit->type == 'copy') 1061 $orig->addWords($edit->closing); 1062 elseif ($edit->type == 'change'){ 1063 $orig->addWords($edit->orig, 'del'); 1064 $orig->addWords($edit->closing, 'add'); 1065 } elseif ($edit->type == 'delete') 1066 $orig->addWords($edit->orig, 'del'); 1067 elseif ($edit->type == 'add') 1068 $orig->addWords($edit->closing, 'add'); 1069 elseif ($edit->orig) 1070 $orig->addWords($edit->orig, 'del'); 1071 } 1072 return $orig->getLines(); 1073 } 1074} 1075 1076/** 1077 * "Unified" diff formatter. 1078 * 1079 * This class formats the diff in classic "unified diff" format. 1080 * 1081 * NOTE: output is plain text and unsafe for use in HTML without escaping. 1082 */ 1083class UnifiedDiffFormatter extends DiffFormatter { 1084 1085 function __construct($context_lines = 4) { 1086 $this->leading_context_lines = $context_lines; 1087 $this->trailing_context_lines = $context_lines; 1088 } 1089 1090 function _block_header($xbeg, $xlen, $ybeg, $ylen) { 1091 if ($xlen != 1) 1092 $xbeg .= "," . $xlen; 1093 if ($ylen != 1) 1094 $ybeg .= "," . $ylen; 1095 return "@@ -$xbeg +$ybeg @@\n"; 1096 } 1097 1098 function _added($lines) { 1099 $this->_lines($lines, "+"); 1100 } 1101 function _deleted($lines) { 1102 $this->_lines($lines, "-"); 1103 } 1104 function _changed($orig, $final) { 1105 $this->_deleted($orig); 1106 $this->_added($final); 1107 } 1108} 1109 1110/** 1111 * Wikipedia Table style diff formatter. 1112 * 1113 */ 1114class TableDiffFormatter extends DiffFormatter { 1115 var $colspan = 2; 1116 1117 function __construct() { 1118 $this->leading_context_lines = 2; 1119 $this->trailing_context_lines = 2; 1120 } 1121 1122 /** 1123 * @param Diff $diff 1124 * @return string 1125 */ 1126 function format($diff) { 1127 // Preserve whitespaces by converting some to non-breaking spaces. 1128 // Do not convert all of them to allow word-wrap. 1129 $val = parent::format($diff); 1130 $val = str_replace(' ','  ', $val); 1131 $val = preg_replace('/ (?=<)|(?<=[ >]) /', ' ', $val); 1132 return $val; 1133 } 1134 1135 function _pre($text){ 1136 $text = htmlspecialchars($text); 1137 return $text; 1138 } 1139 1140 function _block_header($xbeg, $xlen, $ybeg, $ylen) { 1141 global $lang; 1142 $l1 = $lang['line'].' '.$xbeg; 1143 $l2 = $lang['line'].' '.$ybeg; 1144 $r = '<tr><td '.HTMLDiff::css('diff-blockheader').' colspan="'.$this->colspan.'">'.$l1.":</td>\n". 1145 '<td '.HTMLDiff::css('diff-blockheader').' colspan="'.$this->colspan.'">'.$l2.":</td>\n". 1146 "</tr>\n"; 1147 return $r; 1148 } 1149 1150 function _start_block($header) { 1151 print($header); 1152 } 1153 1154 function _end_block() { 1155 } 1156 1157 function _lines($lines, $prefix=' ', $color="white") { 1158 } 1159 1160 function addedLine($line,$escaped=false) { 1161 if (!$escaped){ 1162 $line = $this->_escape($line); 1163 } 1164 return '<td '.HTMLDiff::css('diff-lineheader').'>+</td>'. 1165 '<td '.HTMLDiff::css('diff-addedline').'>' . $line.'</td>'; 1166 } 1167 1168 function deletedLine($line,$escaped=false) { 1169 if (!$escaped){ 1170 $line = $this->_escape($line); 1171 } 1172 return '<td '.HTMLDiff::css('diff-lineheader').'>-</td>'. 1173 '<td '.HTMLDiff::css('diff-deletedline').'>' . $line.'</td>'; 1174 } 1175 1176 function emptyLine() { 1177 return '<td colspan="'.$this->colspan.'"> </td>'; 1178 } 1179 1180 function contextLine($line) { 1181 return '<td '.HTMLDiff::css('diff-lineheader').'> </td>'. 1182 '<td '.HTMLDiff::css('diff-context').'>'.$this->_escape($line).'</td>'; 1183 } 1184 1185 function _added($lines) { 1186 $this->_addedLines($lines,false); 1187 } 1188 1189 function _addedLines($lines,$escaped=false){ 1190 foreach ($lines as $line) { 1191 print('<tr>' . $this->emptyLine() . $this->addedLine($line,$escaped) . "</tr>\n"); 1192 } 1193 } 1194 1195 function _deleted($lines) { 1196 foreach ($lines as $line) { 1197 print('<tr>' . $this->deletedLine($line) . $this->emptyLine() . "</tr>\n"); 1198 } 1199 } 1200 1201 function _context($lines) { 1202 foreach ($lines as $line) { 1203 print('<tr>' . $this->contextLine($line) . $this->contextLine($line) . "</tr>\n"); 1204 } 1205 } 1206 1207 function _changed($orig, $closing) { 1208 $diff = new WordLevelDiff($orig, $closing); // this escapes the diff data 1209 $del = $diff->orig(); 1210 $add = $diff->closing(); 1211 1212 while ($line = array_shift($del)) { 1213 $aline = array_shift($add); 1214 print('<tr>' . $this->deletedLine($line,true) . $this->addedLine($aline,true) . "</tr>\n"); 1215 } 1216 $this->_addedLines($add,true); # If any leftovers 1217 } 1218 1219 function _escape($str) { 1220 return hsc($str); 1221 } 1222} 1223 1224/** 1225 * Inline style diff formatter. 1226 * 1227 */ 1228class InlineDiffFormatter extends DiffFormatter { 1229 var $colspan = 2; 1230 1231 function __construct() { 1232 $this->leading_context_lines = 2; 1233 $this->trailing_context_lines = 2; 1234 } 1235 1236 /** 1237 * @param Diff $diff 1238 * @return string 1239 */ 1240 function format($diff) { 1241 // Preserve whitespaces by converting some to non-breaking spaces. 1242 // Do not convert all of them to allow word-wrap. 1243 $val = parent::format($diff); 1244 $val = str_replace(' ','  ', $val); 1245 $val = preg_replace('/ (?=<)|(?<=[ >]) /', ' ', $val); 1246 return $val; 1247 } 1248 1249 function _pre($text){ 1250 $text = htmlspecialchars($text); 1251 return $text; 1252 } 1253 1254 function _block_header($xbeg, $xlen, $ybeg, $ylen) { 1255 global $lang; 1256 if ($xlen != 1) 1257 $xbeg .= "," . $xlen; 1258 if ($ylen != 1) 1259 $ybeg .= "," . $ylen; 1260 $r = '<tr><td colspan="'.$this->colspan.'" '.HTMLDiff::css('diff-blockheader').'>@@ '.$lang['line']." -$xbeg +$ybeg @@"; 1261 $r .= ' <span '.HTMLDiff::css('diff-deletedline').'><del>'.$lang['deleted'].'</del></span>'; 1262 $r .= ' <span '.HTMLDiff::css('diff-addedline').'>'.$lang['created'].'</span>'; 1263 $r .= "</td></tr>\n"; 1264 return $r; 1265 } 1266 1267 function _start_block($header) { 1268 print($header."\n"); 1269 } 1270 1271 function _end_block() { 1272 } 1273 1274 function _lines($lines, $prefix=' ', $color="white") { 1275 } 1276 1277 function _added($lines) { 1278 foreach ($lines as $line) { 1279 print('<tr><td '.HTMLDiff::css('diff-lineheader').'> </td><td '.HTMLDiff::css('diff-addedline').'>'. $this->_escape($line) . "</td></tr>\n"); 1280 } 1281 } 1282 1283 function _deleted($lines) { 1284 foreach ($lines as $line) { 1285 print('<tr><td '.HTMLDiff::css('diff-lineheader').'> </td><td '.HTMLDiff::css('diff-deletedline').'><del>' . $this->_escape($line) . "</del></td></tr>\n"); 1286 } 1287 } 1288 1289 function _context($lines) { 1290 foreach ($lines as $line) { 1291 print('<tr><td '.HTMLDiff::css('diff-lineheader').'> </td><td '.HTMLDiff::css('diff-context').'>'. $this->_escape($line) ."</td></tr>\n"); 1292 } 1293 } 1294 1295 function _changed($orig, $closing) { 1296 $diff = new InlineWordLevelDiff($orig, $closing); // this escapes the diff data 1297 $add = $diff->inline(); 1298 1299 foreach ($add as $line) 1300 print('<tr><td '.HTMLDiff::css('diff-lineheader').'> </td><td>'.$line."</td></tr>\n"); 1301 } 1302 1303 function _escape($str) { 1304 return hsc($str); 1305 } 1306} 1307 1308/** 1309 * A class for computing three way diffs. 1310 * 1311 * @author Geoffrey T. Dairiki <dairiki@dairiki.org> 1312 */ 1313class Diff3 extends Diff { 1314 1315 /** 1316 * Conflict counter. 1317 * 1318 * @var integer 1319 */ 1320 var $_conflictingBlocks = 0; 1321 1322 /** 1323 * Computes diff between 3 sequences of strings. 1324 * 1325 * @param array $orig The original lines to use. 1326 * @param array $final1 The first version to compare to. 1327 * @param array $final2 The second version to compare to. 1328 */ 1329 function __construct($orig, $final1, $final2) { 1330 $engine = new _DiffEngine(); 1331 1332 $this->_edits = $this->_diff3($engine->diff($orig, $final1), 1333 $engine->diff($orig, $final2)); 1334 } 1335 1336 /** 1337 * Returns the merged lines 1338 * 1339 * @param string $label1 label for first version 1340 * @param string $label2 label for second version 1341 * @param string $label3 separator between versions 1342 * @return array lines of the merged text 1343 */ 1344 function mergedOutput($label1='<<<<<<<',$label2='>>>>>>>',$label3='=======') { 1345 $lines = array(); 1346 foreach ($this->_edits as $edit) { 1347 if ($edit->isConflict()) { 1348 /* FIXME: this should probably be moved somewhere else. */ 1349 $lines = array_merge($lines, 1350 array($label1), 1351 $edit->final1, 1352 array($label3), 1353 $edit->final2, 1354 array($label2)); 1355 $this->_conflictingBlocks++; 1356 } else { 1357 $lines = array_merge($lines, $edit->merged()); 1358 } 1359 } 1360 1361 return $lines; 1362 } 1363 1364 /** 1365 * @access private 1366 * 1367 * @param array $edits1 1368 * @param array $edits2 1369 * 1370 * @return array 1371 */ 1372 function _diff3($edits1, $edits2) { 1373 $edits = array(); 1374 $bb = new _Diff3_BlockBuilder(); 1375 1376 $e1 = current($edits1); 1377 $e2 = current($edits2); 1378 while ($e1 || $e2) { 1379 if ($e1 && $e2 && is_a($e1, '_DiffOp_copy') && is_a($e2, '_DiffOp_copy')) { 1380 /* We have copy blocks from both diffs. This is the (only) 1381 * time we want to emit a diff3 copy block. Flush current 1382 * diff3 diff block, if any. */ 1383 if ($edit = $bb->finish()) { 1384 $edits[] = $edit; 1385 } 1386 1387 $ncopy = min($e1->norig(), $e2->norig()); 1388 assert($ncopy > 0); 1389 $edits[] = new _Diff3_Op_copy(array_slice($e1->orig, 0, $ncopy)); 1390 1391 if ($e1->norig() > $ncopy) { 1392 array_splice($e1->orig, 0, $ncopy); 1393 array_splice($e1->closing, 0, $ncopy); 1394 } else { 1395 $e1 = next($edits1); 1396 } 1397 1398 if ($e2->norig() > $ncopy) { 1399 array_splice($e2->orig, 0, $ncopy); 1400 array_splice($e2->closing, 0, $ncopy); 1401 } else { 1402 $e2 = next($edits2); 1403 } 1404 } else { 1405 if ($e1 && $e2) { 1406 if ($e1->orig && $e2->orig) { 1407 $norig = min($e1->norig(), $e2->norig()); 1408 $orig = array_splice($e1->orig, 0, $norig); 1409 array_splice($e2->orig, 0, $norig); 1410 $bb->input($orig); 1411 } 1412 1413 if (is_a($e1, '_DiffOp_copy')) { 1414 $bb->out1(array_splice($e1->closing, 0, $norig)); 1415 } 1416 1417 if (is_a($e2, '_DiffOp_copy')) { 1418 $bb->out2(array_splice($e2->closing, 0, $norig)); 1419 } 1420 } 1421 1422 if ($e1 && ! $e1->orig) { 1423 $bb->out1($e1->closing); 1424 $e1 = next($edits1); 1425 } 1426 if ($e2 && ! $e2->orig) { 1427 $bb->out2($e2->closing); 1428 $e2 = next($edits2); 1429 } 1430 } 1431 } 1432 1433 if ($edit = $bb->finish()) { 1434 $edits[] = $edit; 1435 } 1436 1437 return $edits; 1438 } 1439} 1440 1441/** 1442 * @author Geoffrey T. Dairiki <dairiki@dairiki.org> 1443 * 1444 * @access private 1445 */ 1446class _Diff3_Op { 1447 1448 function __construct($orig = false, $final1 = false, $final2 = false) { 1449 $this->orig = $orig ? $orig : array(); 1450 $this->final1 = $final1 ? $final1 : array(); 1451 $this->final2 = $final2 ? $final2 : array(); 1452 } 1453 1454 function merged() { 1455 if (!isset($this->_merged)) { 1456 if ($this->final1 === $this->final2) { 1457 $this->_merged = &$this->final1; 1458 } elseif ($this->final1 === $this->orig) { 1459 $this->_merged = &$this->final2; 1460 } elseif ($this->final2 === $this->orig) { 1461 $this->_merged = &$this->final1; 1462 } else { 1463 $this->_merged = false; 1464 } 1465 } 1466 1467 return $this->_merged; 1468 } 1469 1470 function isConflict() { 1471 return $this->merged() === false; 1472 } 1473 1474} 1475 1476/** 1477 * @author Geoffrey T. Dairiki <dairiki@dairiki.org> 1478 * 1479 * @access private 1480 */ 1481class _Diff3_Op_copy extends _Diff3_Op { 1482 1483 function __construct($lines = false) { 1484 $this->orig = $lines ? $lines : array(); 1485 $this->final1 = &$this->orig; 1486 $this->final2 = &$this->orig; 1487 } 1488 1489 function merged() { 1490 return $this->orig; 1491 } 1492 1493 function isConflict() { 1494 return false; 1495 } 1496} 1497 1498/** 1499 * @author Geoffrey T. Dairiki <dairiki@dairiki.org> 1500 * 1501 * @access private 1502 */ 1503class _Diff3_BlockBuilder { 1504 1505 function __construct() { 1506 $this->_init(); 1507 } 1508 1509 function input($lines) { 1510 if ($lines) { 1511 $this->_append($this->orig, $lines); 1512 } 1513 } 1514 1515 function out1($lines) { 1516 if ($lines) { 1517 $this->_append($this->final1, $lines); 1518 } 1519 } 1520 1521 function out2($lines) { 1522 if ($lines) { 1523 $this->_append($this->final2, $lines); 1524 } 1525 } 1526 1527 function isEmpty() { 1528 return !$this->orig && !$this->final1 && !$this->final2; 1529 } 1530 1531 function finish() { 1532 if ($this->isEmpty()) { 1533 return false; 1534 } else { 1535 $edit = new _Diff3_Op($this->orig, $this->final1, $this->final2); 1536 $this->_init(); 1537 return $edit; 1538 } 1539 } 1540 1541 function _init() { 1542 $this->orig = $this->final1 = $this->final2 = array(); 1543 } 1544 1545 function _append(&$array, $lines) { 1546 array_splice($array, sizeof($array), 0, $lines); 1547 } 1548} 1549 1550//Setup VIM: ex: et ts=4 : 1551