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