Main Page | Namespace List | Class Hierarchy | Class List | File List | Class Members | File Members | Related Pages

DifferenceEngine.php

Go to the documentation of this file.
00001 <?php 00002 # See diff.doc 00003 00004 class DifferenceEngine { 00005 /* private */ var $mOldid, $mNewid; 00006 /* private */ var $mOldtitle, $mNewtitle; 00007 /* private */ var $mOldtext, $mNewtext; 00008 /* private */ var $mOldUser, $mNewUser; 00009 /* private */ var $mOldComment, $mNewComment; 00010 /* private */ var $mOldPage, $mNewPage; 00011 00012 function DifferenceEngine( $old, $new ) 00013 { 00014 $this->mOldid = $old; 00015 $this->mNewid = $new; 00016 } 00017 00018 function showDiffPage() 00019 { 00020 global $wgUser, $wgTitle, $wgOut, $wgLang; 00021 $fname = "DifferenceEngine::showDiffPage"; 00022 wfProfileIn( $fname ); 00023 00024 $t = $wgTitle->getPrefixedText() . " (Diff: {$this->mOldid}, " . 00025 "{$this->mNewid})"; 00026 $mtext = wfMsg( "missingarticle", $t ); 00027 00028 $wgOut->setArticleFlag( false ); 00029 if ( ! $this->loadText() ) { 00030 $wgOut->setPagetitle( wfMsg( "errorpagetitle" ) ); 00031 $wgOut->addHTML( $mtext ); 00032 wfProfileOut( $fname ); 00033 return; 00034 } 00035 $wgOut->suppressQuickbar(); 00036 00037 $oldTitle = $this->mOldPage->getPrefixedText(); 00038 $newTitle = $this->mNewPage->getPrefixedText(); 00039 if( $oldTitle == $newTitle ) { 00040 $wgOut->setPageTitle( $newTitle ); 00041 } else { 00042 $wgOut->setPageTitle( $oldTitle . ", " . $newTitle ); 00043 } 00044 $wgOut->setSubtitle( wfMsg( "difference" ) ); 00045 $wgOut->setRobotpolicy( "noindex,follow" ); 00046 00047 if ( !( $this->mOldPage->userCanRead() && $this->mNewPage->userCanRead() ) ) { 00048 $wgOut->loginToUse(); 00049 $wgOut->output(); 00050 wfProfileOut( $fname ); 00051 exit; 00052 } 00053 00054 $sk = $wgUser->getSkin(); 00055 $talk = $wgLang->getNsText( NS_TALK ); 00056 $contribs = wfMsg( "contribslink" ); 00057 00058 00059 $this->mOldComment = $sk->formatComment($this->mOldComment); 00060 $this->mNewComment = $sk->formatComment($this->mNewComment); 00061 00062 00063 $oldUserLink = $sk->makeLinkObj( Title::makeTitle( NS_USER, $this->mOldUser ), $this->mOldUser ); 00064 $newUserLink = $sk->makeLinkObj( Title::makeTitle( NS_USER, $this->mNewUser ), $this->mNewUser ); 00065 $oldUTLink = $sk->makeLinkObj( Title::makeTitle( NS_USER_TALK, $this->mOldUser ), $talk ); 00066 $newUTLink = $sk->makeLinkObj( Title::makeTitle( NS_USER_TALK, $this->mNewUser ), $talk ); 00067 $oldContribs = $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, "Contributions" ), $contribs, 00068 "target=" . urlencode($this->mOldUser) ); 00069 $newContribs = $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, "Contributions" ), $contribs, 00070 "target=" . urlencode($this->mNewUser) ); 00071 if ( !$this->mNewid && $wgUser->isSysop() ) { 00072 $rollback = "&nbsp;&nbsp;&nbsp;<strong>[" . $sk->makeKnownLinkObj( $wgTitle, wfMsg( "rollbacklink" ), 00073 "action=rollback&from=" . urlencode($this->mNewUser) ) . "]</strong>"; 00074 } else { 00075 $rollback = ""; 00076 } 00077 00078 $oldHeader = "<strong>{$this->mOldtitle}</strong><br />$oldUserLink ($oldUTLink | $oldContribs)<br />". 00079 $this->mOldComment; 00080 $newHeader = "<strong>{$this->mNewtitle}</strong><br />$newUserLink ($newUTLink | $newContribs) $rollback<br />". 00081 $this->mNewComment; 00082 00083 DifferenceEngine::showDiff( $this->mOldtext, $this->mNewtext, 00084 $oldHeader, $newHeader ); 00085 $wgOut->addHTML( "<hr /><h2>{$this->mNewtitle}</h2>\n" ); 00086 $wgOut->addWikiText( $this->mNewtext ); 00087 00088 wfProfileOut( $fname ); 00089 } 00090 00091 function showDiff( $otext, $ntext, $otitle, $ntitle ) 00092 { 00093 global $wgOut; 00094 00095 $ota = explode( "\n", str_replace( "\r\n", "\n", 00096 htmlspecialchars( $otext ) ) ); 00097 $nta = explode( "\n", str_replace( "\r\n", "\n", 00098 htmlspecialchars( $ntext ) ) ); 00099 00100 $wgOut->addHTML( "<table border='0' width='98%' 00101 cellpadding='0' cellspacing='4px' class='diff'><tr> 00102 <td colspan='2' width='50%' align='center' class='diff-otitle'> 00103 {$otitle}</td> 00104 <td colspan='2' width='50%' align='center' class='diff-ntitle'> 00105 {$ntitle}</td> 00106 </tr>\n" ); 00107 00108 $diffs = new Diff( $ota, $nta ); 00109 $formatter = new TableDiffFormatter(); 00110 $formatter->format( $diffs ); 00111 $wgOut->addHTML( "</table>\n" ); 00112 } 00113 00114 # Load the text of the articles to compare. If newid is 0, then compare 00115 # the old article in oldid to the current article; if oldid is 0, then 00116 # compare the current article to the immediately previous one (ignoring 00117 # the value of newid). 00118 # 00119 function loadText() 00120 { 00121 global $wgTitle, $wgOut, $wgLang, $wgIsMySQL, $wgIsPg; 00122 $fname = "DifferenceEngine::loadText"; 00123 00124 $oldtable=$wgIsPg?'"old"':'old'; 00125 if ( 0 == $this->mNewid || 0 == $this->mOldid ) { 00126 $wgOut->setArticleFlag( true ); 00127 $this->mNewtitle = wfMsg( "currentrev" ); 00128 $id = $wgTitle->getArticleID(); 00129 00130 $sql = "SELECT cur_text, cur_user_text, cur_comment FROM cur WHERE cur_id={$id}"; 00131 $res = wfQuery( $sql, DB_READ, $fname ); 00132 if ( 0 == wfNumRows( $res ) ) { return false; } 00133 00134 $s = wfFetchObject( $res ); 00135 $this->mNewPage = &$wgTitle; 00136 $this->mNewtext = $s->cur_text; 00137 $this->mNewUser = $s->cur_user_text; 00138 $this->mNewComment = $s->cur_comment; 00139 } else { 00140 $sql = "SELECT old_namespace,old_title,old_timestamp,old_text,old_flags,old_user_text,old_comment FROM $oldtable WHERE " . 00141 "old_id={$this->mNewid}"; 00142 00143 $res = wfQuery( $sql, DB_READ, $fname ); 00144 if ( 0 == wfNumRows( $res ) ) { return false; } 00145 00146 $s = wfFetchObject( $res ); 00147 $this->mNewtext = Article::getRevisionText( $s ); 00148 00149 $t = $wgLang->timeanddate( $s->old_timestamp, true ); 00150 $this->mNewPage = Title::MakeTitle( $s->old_namespace, $s->old_title ); 00151 $this->mNewtitle = wfMsg( "revisionasof", $t ); 00152 $this->mNewUser = $s->old_user_text; 00153 $this->mNewComment = $s->old_comment; 00154 } 00155 if ( 0 == $this->mOldid ) { 00156 $use_index=$wgIsMySQL?"USE INDEX (name_title_timestamp)":""; 00157 $sql = "SELECT old_namespace,old_title,old_timestamp,old_text,old_flags,old_user_text,old_comment " . 00158 "FROM $oldtable $use_index WHERE " . 00159 "old_namespace=" . $this->mNewPage->getNamespace() . " AND " . 00160 "old_title='" . wfStrencode( $this->mNewPage->getDBkey() ) . 00161 "' ORDER BY inverse_timestamp LIMIT 1"; 00162 $res = wfQuery( $sql, DB_READ, $fname ); 00163 } else { 00164 $sql = "SELECT old_namespace,old_title,old_timestamp,old_text,old_flags,old_user_text,old_comment FROM $oldtable WHERE " . 00165 "old_id={$this->mOldid}"; 00166 $res = wfQuery( $sql, DB_READ, $fname ); 00167 } 00168 if ( 0 == wfNumRows( $res ) ) { return false; } 00169 00170 $s = wfFetchObject( $res ); 00171 $this->mOldPage = Title::MakeTitle( $s->old_namespace, $s->old_title ); 00172 $this->mOldtext = Article::getRevisionText( $s ); 00173 00174 $t = $wgLang->timeanddate( $s->old_timestamp, true ); 00175 $this->mOldtitle = wfMsg( "revisionasof", $t ); 00176 $this->mOldUser = $s->old_user_text; 00177 $this->mOldComment = $s->old_comment; 00178 00179 return true; 00180 } 00181 } 00182 00183 // A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3) 00184 // 00185 // Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org> 00186 // You may copy this code freely under the conditions of the GPL. 00187 // 00188 00189 define('USE_ASSERTS', function_exists('assert')); 00190 00191 class _DiffOp { 00192 var $type; 00193 var $orig; 00194 var $closing; 00195 00196 function reverse() { 00197 trigger_error("pure virtual", E_USER_ERROR); 00198 } 00199 00200 function norig() { 00201 return $this->orig ? sizeof($this->orig) : 0; 00202 } 00203 00204 function nclosing() { 00205 return $this->closing ? sizeof($this->closing) : 0; 00206 } 00207 } 00208 00209 class _DiffOp_Copy extends _DiffOp { 00210 var $type = 'copy'; 00211 00212 function _DiffOp_Copy ($orig, $closing = false) { 00213 if (!is_array($closing)) 00214 $closing = $orig; 00215 $this->orig = $orig; 00216 $this->closing = $closing; 00217 } 00218 00219 function reverse() { 00220 return new _DiffOp_Copy($this->closing, $this->orig); 00221 } 00222 } 00223 00224 class _DiffOp_Delete extends _DiffOp { 00225 var $type = 'delete'; 00226 00227 function _DiffOp_Delete ($lines) { 00228 $this->orig = $lines; 00229 $this->closing = false; 00230 } 00231 00232 function reverse() { 00233 return new _DiffOp_Add($this->orig); 00234 } 00235 } 00236 00237 class _DiffOp_Add extends _DiffOp { 00238 var $type = 'add'; 00239 00240 function _DiffOp_Add ($lines) { 00241 $this->closing = $lines; 00242 $this->orig = false; 00243 } 00244 00245 function reverse() { 00246 return new _DiffOp_Delete($this->closing); 00247 } 00248 } 00249 00250 class _DiffOp_Change extends _DiffOp { 00251 var $type = 'change'; 00252 00253 function _DiffOp_Change ($orig, $closing) { 00254 $this->orig = $orig; 00255 $this->closing = $closing; 00256 } 00257 00258 function reverse() { 00259 return new _DiffOp_Change($this->closing, $this->orig); 00260 } 00261 } 00262 00263 00284 class _DiffEngine 00285 { 00286 function diff ($from_lines, $to_lines) { 00287 $n_from = sizeof($from_lines); 00288 $n_to = sizeof($to_lines); 00289 00290 $this->xchanged = $this->ychanged = array(); 00291 $this->xv = $this->yv = array(); 00292 $this->xind = $this->yind = array(); 00293 unset($this->seq); 00294 unset($this->in_seq); 00295 unset($this->lcs); 00296 00297 // Skip leading common lines. 00298 for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) { 00299 if ($from_lines[$skip] != $to_lines[$skip]) 00300 break; 00301 $this->xchanged[$skip] = $this->ychanged[$skip] = false; 00302 } 00303 // Skip trailing common lines. 00304 $xi = $n_from; $yi = $n_to; 00305 for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) { 00306 if ($from_lines[$xi] != $to_lines[$yi]) 00307 break; 00308 $this->xchanged[$xi] = $this->ychanged[$yi] = false; 00309 } 00310 00311 // Ignore lines which do not exist in both files. 00312 for ($xi = $skip; $xi < $n_from - $endskip; $xi++) 00313 $xhash[$from_lines[$xi]] = 1; 00314 for ($yi = $skip; $yi < $n_to - $endskip; $yi++) { 00315 $line = $to_lines[$yi]; 00316 if ( ($this->ychanged[$yi] = empty($xhash[$line])) ) 00317 continue; 00318 $yhash[$line] = 1; 00319 $this->yv[] = $line; 00320 $this->yind[] = $yi; 00321 } 00322 for ($xi = $skip; $xi < $n_from - $endskip; $xi++) { 00323 $line = $from_lines[$xi]; 00324 if ( ($this->xchanged[$xi] = empty($yhash[$line])) ) 00325 continue; 00326 $this->xv[] = $line; 00327 $this->xind[] = $xi; 00328 } 00329 00330 // Find the LCS. 00331 $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv)); 00332 00333 // Merge edits when possible 00334 $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged); 00335 $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged); 00336 00337 // Compute the edit operations. 00338 $edits = array(); 00339 $xi = $yi = 0; 00340 while ($xi < $n_from || $yi < $n_to) { 00341 USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]); 00342 USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]); 00343 00344 // Skip matching "snake". 00345 $copy = array(); 00346 while ( $xi < $n_from && $yi < $n_to 00347 && !$this->xchanged[$xi] && !$this->ychanged[$yi]) { 00348 $copy[] = $from_lines[$xi++]; 00349 ++$yi; 00350 } 00351 if ($copy) 00352 $edits[] = new _DiffOp_Copy($copy); 00353 00354 // Find deletes & adds. 00355 $delete = array(); 00356 while ($xi < $n_from && $this->xchanged[$xi]) 00357 $delete[] = $from_lines[$xi++]; 00358 00359 $add = array(); 00360 while ($yi < $n_to && $this->ychanged[$yi]) 00361 $add[] = $to_lines[$yi++]; 00362 00363 if ($delete && $add) 00364 $edits[] = new _DiffOp_Change($delete, $add); 00365 elseif ($delete) 00366 $edits[] = new _DiffOp_Delete($delete); 00367 elseif ($add) 00368 $edits[] = new _DiffOp_Add($add); 00369 } 00370 return $edits; 00371 } 00372 00373 00374 /* Divide the Largest Common Subsequence (LCS) of the sequences 00375 * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally 00376 * sized segments. 00377 * 00378 * Returns (LCS, PTS). LCS is the length of the LCS. PTS is an 00379 * array of NCHUNKS+1 (X, Y) indexes giving the diving points between 00380 * sub sequences. The first sub-sequence is contained in [X0, X1), 00381 * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on. Note 00382 * that (X0, Y0) == (XOFF, YOFF) and 00383 * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM). 00384 * 00385 * This function assumes that the first lines of the specified portions 00386 * of the two files do not match, and likewise that the last lines do not 00387 * match. The caller must trim matching lines from the beginning and end 00388 * of the portions it is going to specify. 00389 */ 00390 function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks) { 00391 $flip = false; 00392 00393 if ($xlim - $xoff > $ylim - $yoff) { 00394 // Things seems faster (I'm not sure I understand why) 00395 // when the shortest sequence in X. 00396 $flip = true; 00397 list ($xoff, $xlim, $yoff, $ylim) 00398 = array( $yoff, $ylim, $xoff, $xlim); 00399 } 00400 00401 if ($flip) 00402 for ($i = $ylim - 1; $i >= $yoff; $i--) 00403 $ymatches[$this->xv[$i]][] = $i; 00404 else 00405 for ($i = $ylim - 1; $i >= $yoff; $i--) 00406 $ymatches[$this->yv[$i]][] = $i; 00407 00408 $this->lcs = 0; 00409 $this->seq[0]= $yoff - 1; 00410 $this->in_seq = array(); 00411 $ymids[0] = array(); 00412 00413 $numer = $xlim - $xoff + $nchunks - 1; 00414 $x = $xoff; 00415 for ($chunk = 0; $chunk < $nchunks; $chunk++) { 00416 if ($chunk > 0) 00417 for ($i = 0; $i <= $this->lcs; $i++) 00418 $ymids[$i][$chunk-1] = $this->seq[$i]; 00419 00420 $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks); 00421 for ( ; $x < $x1; $x++) { 00422 $line = $flip ? $this->yv[$x] : $this->xv[$x]; 00423 if (empty($ymatches[$line])) 00424 continue; 00425 $matches = $ymatches[$line]; 00426 reset($matches); 00427 while (list ($junk, $y) = each($matches)) 00428 if (empty($this->in_seq[$y])) { 00429 $k = $this->_lcs_pos($y); 00430 USE_ASSERTS && assert($k > 0); 00431 $ymids[$k] = $ymids[$k-1]; 00432 break; 00433 } 00434 while (list ($junk, $y) = each($matches)) { 00435 if ($y > $this->seq[$k-1]) { 00436 USE_ASSERTS && assert($y < $this->seq[$k]); 00437 // Optimization: this is a common case: 00438 // next match is just replacing previous match. 00439 $this->in_seq[$this->seq[$k]] = false; 00440 $this->seq[$k] = $y; 00441 $this->in_seq[$y] = 1; 00442 } 00443 else if (empty($this->in_seq[$y])) { 00444 $k = $this->_lcs_pos($y); 00445 USE_ASSERTS && assert($k > 0); 00446 $ymids[$k] = $ymids[$k-1]; 00447 } 00448 } 00449 } 00450 } 00451 00452 $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff); 00453 $ymid = $ymids[$this->lcs]; 00454 for ($n = 0; $n < $nchunks - 1; $n++) { 00455 $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks); 00456 $y1 = $ymid[$n] + 1; 00457 $seps[] = $flip ? array($y1, $x1) : array($x1, $y1); 00458 } 00459 $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim); 00460 00461 return array($this->lcs, $seps); 00462 } 00463 00464 function _lcs_pos ($ypos) { 00465 $end = $this->lcs; 00466 if ($end == 0 || $ypos > $this->seq[$end]) { 00467 $this->seq[++$this->lcs] = $ypos; 00468 $this->in_seq[$ypos] = 1; 00469 return $this->lcs; 00470 } 00471 00472 $beg = 1; 00473 while ($beg < $end) { 00474 $mid = (int)(($beg + $end) / 2); 00475 if ( $ypos > $this->seq[$mid] ) 00476 $beg = $mid + 1; 00477 else 00478 $end = $mid; 00479 } 00480 00481 USE_ASSERTS && assert($ypos != $this->seq[$end]); 00482 00483 $this->in_seq[$this->seq[$end]] = false; 00484 $this->seq[$end] = $ypos; 00485 $this->in_seq[$ypos] = 1; 00486 return $end; 00487 } 00488 00489 /* Find LCS of two sequences. 00490 * 00491 * The results are recorded in the vectors $this->{x,y}changed[], by 00492 * storing a 1 in the element for each line that is an insertion 00493 * or deletion (ie. is not in the LCS). 00494 * 00495 * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1. 00496 * 00497 * Note that XLIM, YLIM are exclusive bounds. 00498 * All line numbers are origin-0 and discarded lines are not counted. 00499 */ 00500 function _compareseq ($xoff, $xlim, $yoff, $ylim) { 00501 // Slide down the bottom initial diagonal. 00502 while ($xoff < $xlim && $yoff < $ylim 00503 && $this->xv[$xoff] == $this->yv[$yoff]) { 00504 ++$xoff; 00505 ++$yoff; 00506 } 00507 00508 // Slide up the top initial diagonal. 00509 while ($xlim > $xoff && $ylim > $yoff 00510 && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) { 00511 --$xlim; 00512 --$ylim; 00513 } 00514 00515 if ($xoff == $xlim || $yoff == $ylim) 00516 $lcs = 0; 00517 else { 00518 // This is ad hoc but seems to work well. 00519 //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5); 00520 //$nchunks = max(2,min(8,(int)$nchunks)); 00521 $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1; 00522 list ($lcs, $seps) 00523 = $this->_diag($xoff,$xlim,$yoff, $ylim,$nchunks); 00524 } 00525 00526 if ($lcs == 0) { 00527 // X and Y sequences have no common subsequence: 00528 // mark all changed. 00529 while ($yoff < $ylim) 00530 $this->ychanged[$this->yind[$yoff++]] = 1; 00531 while ($xoff < $xlim) 00532 $this->xchanged[$this->xind[$xoff++]] = 1; 00533 } 00534 else { 00535 // Use the partitions to split this problem into subproblems. 00536 reset($seps); 00537 $pt1 = $seps[0]; 00538 while ($pt2 = next($seps)) { 00539 $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]); 00540 $pt1 = $pt2; 00541 } 00542 } 00543 } 00544 00545 /* Adjust inserts/deletes of identical lines to join changes 00546 * as much as possible. 00547 * 00548 * We do something when a run of changed lines include a 00549 * line at one end and has an excluded, identical line at the other. 00550 * We are free to choose which identical line is included. 00551 * `compareseq' usually chooses the one at the beginning, 00552 * but usually it is cleaner to consider the following identical line 00553 * to be the "change". 00554 * 00555 * This is extracted verbatim from analyze.c (GNU diffutils-2.7). 00556 */ 00557 function _shift_boundaries ($lines, &$changed, $other_changed) { 00558 $i = 0; 00559 $j = 0; 00560 00561 USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)'); 00562 $len = sizeof($lines); 00563 $other_len = sizeof($other_changed); 00564 00565 while (1) { 00566 /* 00567 * Scan forwards to find beginning of another run of changes. 00568 * Also keep track of the corresponding point in the other file. 00569 * 00570 * Throughout this code, $i and $j are adjusted together so that 00571 * the first $i elements of $changed and the first $j elements 00572 * of $other_changed both contain the same number of zeros 00573 * (unchanged lines). 00574 * Furthermore, $j is always kept so that $j == $other_len or 00575 * $other_changed[$j] == false. 00576 */ 00577 while ($j < $other_len && $other_changed[$j]) 00578 $j++; 00579 00580 while ($i < $len && ! $changed[$i]) { 00581 USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]'); 00582 $i++; $j++; 00583 while ($j < $other_len && $other_changed[$j]) 00584 $j++; 00585 } 00586 00587 if ($i == $len) 00588 break; 00589 00590 $start = $i; 00591 00592 // Find the end of this run of changes. 00593 while (++$i < $len && $changed[$i]) 00594 continue; 00595 00596 do { 00597 /* 00598 * Record the length of this run of changes, so that 00599 * we can later determine whether the run has grown. 00600 */ 00601 $runlength = $i - $start; 00602 00603 /* 00604 * Move the changed region back, so long as the 00605 * previous unchanged line matches the last changed one. 00606 * This merges with previous changed regions. 00607 */ 00608 while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) { 00609 $changed[--$start] = 1; 00610 $changed[--$i] = false; 00611 while ($start > 0 && $changed[$start - 1]) 00612 $start--; 00613 USE_ASSERTS && assert('$j > 0'); 00614 while ($other_changed[--$j]) 00615 continue; 00616 USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]'); 00617 } 00618 00619 /* 00620 * Set CORRESPONDING to the end of the changed run, at the last 00621 * point where it corresponds to a changed run in the other file. 00622 * CORRESPONDING == LEN means no such point has been found. 00623 */ 00624 $corresponding = $j < $other_len ? $i : $len; 00625 00626 /* 00627 * Move the changed region forward, so long as the 00628 * first changed line matches the following unchanged one. 00629 * This merges with following changed regions. 00630 * Do this second, so that if there are no merges, 00631 * the changed region is moved forward as far as possible. 00632 */ 00633 while ($i < $len && $lines[$start] == $lines[$i]) { 00634 $changed[$start++] = false; 00635 $changed[$i++] = 1; 00636 while ($i < $len && $changed[$i]) 00637 $i++; 00638 00639 USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]'); 00640 $j++; 00641 if ($j < $other_len && $other_changed[$j]) { 00642 $corresponding = $i; 00643 while ($j < $other_len && $other_changed[$j]) 00644 $j++; 00645 } 00646 } 00647 } while ($runlength != $i - $start); 00648 00649 /* 00650 * If possible, move the fully-merged run of changes 00651 * back to a corresponding run in the other file. 00652 */ 00653 while ($corresponding < $i) { 00654 $changed[--$start] = 1; 00655 $changed[--$i] = 0; 00656 USE_ASSERTS && assert('$j > 0'); 00657 while ($other_changed[--$j]) 00658 continue; 00659 USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]'); 00660 } 00661 } 00662 } 00663 } 00664 00668 class Diff 00669 { 00670 var $edits; 00671 00680 function Diff($from_lines, $to_lines) { 00681 $eng = new _DiffEngine; 00682 $this->edits = $eng->diff($from_lines, $to_lines); 00683 //$this->_check($from_lines, $to_lines); 00684 } 00685 00696 function reverse () { 00697 $rev = $this; 00698 $rev->edits = array(); 00699 foreach ($this->edits as $edit) { 00700 $rev->edits[] = $edit->reverse(); 00701 } 00702 return $rev; 00703 } 00704 00710 function isEmpty () { 00711 foreach ($this->edits as $edit) { 00712 if ($edit->type != 'copy') 00713 return false; 00714 } 00715 return true; 00716 } 00717 00725 function lcs () { 00726 $lcs = 0; 00727 foreach ($this->edits as $edit) { 00728 if ($edit->type == 'copy') 00729 $lcs += sizeof($edit->orig); 00730 } 00731 return $lcs; 00732 } 00733 00742 function orig() { 00743 $lines = array(); 00744 00745 foreach ($this->edits as $edit) { 00746 if ($edit->orig) 00747 array_splice($lines, sizeof($lines), 0, $edit->orig); 00748 } 00749 return $lines; 00750 } 00751 00760 function closing() { 00761 $lines = array(); 00762 00763 foreach ($this->edits as $edit) { 00764 if ($edit->closing) 00765 array_splice($lines, sizeof($lines), 0, $edit->closing); 00766 } 00767 return $lines; 00768 } 00769 00775 function _check ($from_lines, $to_lines) { 00776 if (serialize($from_lines) != serialize($this->orig())) 00777 trigger_error("Reconstructed original doesn't match", E_USER_ERROR); 00778 if (serialize($to_lines) != serialize($this->closing())) 00779 trigger_error("Reconstructed closing doesn't match", E_USER_ERROR); 00780 00781 $rev = $this->reverse(); 00782 if (serialize($to_lines) != serialize($rev->orig())) 00783 trigger_error("Reversed original doesn't match", E_USER_ERROR); 00784 if (serialize($from_lines) != serialize($rev->closing())) 00785 trigger_error("Reversed closing doesn't match", E_USER_ERROR); 00786 00787 00788 $prevtype = 'none'; 00789 foreach ($this->edits as $edit) { 00790 if ( $prevtype == $edit->type ) 00791 trigger_error("Edit sequence is non-optimal", E_USER_ERROR); 00792 $prevtype = $edit->type; 00793 } 00794 00795 $lcs = $this->lcs(); 00796 trigger_error("Diff okay: LCS = $lcs", E_USER_NOTICE); 00797 } 00798 } 00799 00803 class MappedDiff 00804 extends Diff 00805 { 00829 function MappedDiff($from_lines, $to_lines, 00830 $mapped_from_lines, $mapped_to_lines) { 00831 00832 assert(sizeof($from_lines) == sizeof($mapped_from_lines)); 00833 assert(sizeof($to_lines) == sizeof($mapped_to_lines)); 00834 00835 $this->Diff($mapped_from_lines, $mapped_to_lines); 00836 00837 $xi = $yi = 0; 00838 for ($i = 0; $i < sizeof($this->edits); $i++) { 00839 $orig = &$this->edits[$i]->orig; 00840 if (is_array($orig)) { 00841 $orig = array_slice($from_lines, $xi, sizeof($orig)); 00842 $xi += sizeof($orig); 00843 } 00844 00845 $closing = &$this->edits[$i]->closing; 00846 if (is_array($closing)) { 00847 $closing = array_slice($to_lines, $yi, sizeof($closing)); 00848 $yi += sizeof($closing); 00849 } 00850 } 00851 } 00852 } 00853 00861 class DiffFormatter 00862 { 00869 var $leading_context_lines = 0; 00870 00877 var $trailing_context_lines = 0; 00878 00885 function format($diff) { 00886 00887 $xi = $yi = 1; 00888 $block = false; 00889 $context = array(); 00890 00891 $nlead = $this->leading_context_lines; 00892 $ntrail = $this->trailing_context_lines; 00893 00894 $this->_start_diff(); 00895 00896 foreach ($diff->edits as $edit) { 00897 if ($edit->type == 'copy') { 00898 if (is_array($block)) { 00899 if (sizeof($edit->orig) <= $nlead + $ntrail) { 00900 $block[] = $edit; 00901 } 00902 else{ 00903 if ($ntrail) { 00904 $context = array_slice($edit->orig, 0, $ntrail); 00905 $block[] = new _DiffOp_Copy($context); 00906 } 00907 $this->_block($x0, $ntrail + $xi - $x0, 00908 $y0, $ntrail + $yi - $y0, 00909 $block); 00910 $block = false; 00911 } 00912 } 00913 $context = $edit->orig; 00914 } 00915 else { 00916 if (! is_array($block)) { 00917 $context = array_slice($context, sizeof($context) - $nlead); 00918 $x0 = $xi - sizeof($context); 00919 $y0 = $yi - sizeof($context); 00920 $block = array(); 00921 if ($context) 00922 $block[] = new _DiffOp_Copy($context); 00923 } 00924 $block[] = $edit; 00925 } 00926 00927 if ($edit->orig) 00928 $xi += sizeof($edit->orig); 00929 if ($edit->closing) 00930 $yi += sizeof($edit->closing); 00931 } 00932 00933 if (is_array($block)) 00934 $this->_block($x0, $xi - $x0, 00935 $y0, $yi - $y0, 00936 $block); 00937 00938 return $this->_end_diff(); 00939 } 00940 00941 function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) { 00942 $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen)); 00943 foreach ($edits as $edit) { 00944 if ($edit->type == 'copy') 00945 $this->_context($edit->orig); 00946 elseif ($edit->type == 'add') 00947 $this->_added($edit->closing); 00948 elseif ($edit->type == 'delete') 00949 $this->_deleted($edit->orig); 00950 elseif ($edit->type == 'change') 00951 $this->_changed($edit->orig, $edit->closing); 00952 else 00953 trigger_error("Unknown edit type", E_USER_ERROR); 00954 } 00955 $this->_end_block(); 00956 } 00957 00958 function _start_diff() { 00959 ob_start(); 00960 } 00961 00962 function _end_diff() { 00963 $val = ob_get_contents(); 00964 ob_end_clean(); 00965 return $val; 00966 } 00967 00968 function _block_header($xbeg, $xlen, $ybeg, $ylen) { 00969 if ($xlen > 1) 00970 $xbeg .= "," . ($xbeg + $xlen - 1); 00971 if ($ylen > 1) 00972 $ybeg .= "," . ($ybeg + $ylen - 1); 00973 00974 return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg; 00975 } 00976 00977 function _start_block($header) { 00978 echo $header; 00979 } 00980 00981 function _end_block() { 00982 } 00983 00984 function _lines($lines, $prefix = ' ') { 00985 foreach ($lines as $line) 00986 echo "$prefix $line\n"; 00987 } 00988 00989 function _context($lines) { 00990 $this->_lines($lines); 00991 } 00992 00993 function _added($lines) { 00994 $this->_lines($lines, ">"); 00995 } 00996 function _deleted($lines) { 00997 $this->_lines($lines, "<"); 00998 } 00999 01000 function _changed($orig, $closing) { 01001 $this->_deleted($orig); 01002 echo "---\n"; 01003 $this->_added($closing); 01004 } 01005 } 01006 01007 01013 define('NBSP', "\xA0"); // iso-8859-x non-breaking space. 01014 01015 class _HWLDF_WordAccumulator { 01016 function _HWLDF_WordAccumulator () { 01017 $this->_lines = array(); 01018 $this->_line = ''; 01019 $this->_group = ''; 01020 $this->_tag = ''; 01021 } 01022 01023 function _flushGroup ($new_tag) { 01024 if ($this->_group !== '') { 01025 if ($this->_tag == 'mark') 01026 $this->_line .= '<span class="diffchange">'.$this->_group.'</span>'; 01027 else 01028 $this->_line .= $this->_group; 01029 } 01030 $this->_group = ''; 01031 $this->_tag = $new_tag; 01032 } 01033 01034 function _flushLine ($new_tag) { 01035 $this->_flushGroup($new_tag); 01036 if ($this->_line != '') 01037 $this->_lines[] = $this->_line; 01038 $this->_line = ''; 01039 } 01040 01041 function addWords ($words, $tag = '') { 01042 if ($tag != $this->_tag) 01043 $this->_flushGroup($tag); 01044 01045 foreach ($words as $word) { 01046 // new-line should only come as first char of word. 01047 if ($word == '') 01048 continue; 01049 if ($word[0] == "\n") { 01050 $this->_group .= NBSP; 01051 $this->_flushLine($tag); 01052 $word = substr($word, 1); 01053 } 01054 assert(!strstr($word, "\n")); 01055 $this->_group .= $word; 01056 } 01057 } 01058 01059 function getLines() { 01060 $this->_flushLine('~done'); 01061 return $this->_lines; 01062 } 01063 } 01064 01065 class WordLevelDiff extends MappedDiff 01066 { 01067 function WordLevelDiff ($orig_lines, $closing_lines) { 01068 list ($orig_words, $orig_stripped) = $this->_split($orig_lines); 01069 list ($closing_words, $closing_stripped) = $this->_split($closing_lines); 01070 01071 01072 $this->MappedDiff($orig_words, $closing_words, 01073 $orig_stripped, $closing_stripped); 01074 } 01075 01076 function _split($lines) { 01077 // FIXME: fix POSIX char class. 01078 # if (!preg_match_all('/ ( [^\S\n]+ | [[:alnum:]]+ | . ) (?: (?!< \n) [^\S\n])? /xs', 01079 if (!preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs', 01080 implode("\n", $lines), 01081 $m)) { 01082 return array(array(''), array('')); 01083 } 01084 return array($m[0], $m[1]); 01085 } 01086 01087 function orig () { 01088 $orig = new _HWLDF_WordAccumulator; 01089 01090 foreach ($this->edits as $edit) { 01091 if ($edit->type == 'copy') 01092 $orig->addWords($edit->orig); 01093 elseif ($edit->orig) 01094 $orig->addWords($edit->orig, 'mark'); 01095 } 01096 return $orig->getLines(); 01097 } 01098 01099 function closing () { 01100 $closing = new _HWLDF_WordAccumulator; 01101 01102 foreach ($this->edits as $edit) { 01103 if ($edit->type == 'copy') 01104 $closing->addWords($edit->closing); 01105 elseif ($edit->closing) 01106 $closing->addWords($edit->closing, 'mark'); 01107 } 01108 return $closing->getLines(); 01109 } 01110 } 01111 01116 class TableDiffFormatter extends DiffFormatter 01117 { 01118 function TableDiffFormatter() { 01119 $this->leading_context_lines = 2; 01120 $this->trailing_context_lines = 2; 01121 } 01122 01123 function _block_header( $xbeg, $xlen, $ybeg, $ylen ) { 01124 $l1 = wfMsg( "lineno", $xbeg ); 01125 $l2 = wfMsg( "lineno", $ybeg ); 01126 01127 $r = '<tr><td colspan="2" align="left"><strong>'.$l1."</strong></td>\n" . 01128 '<td colspan="2" align="left"><strong>'.$l2."</strong></td></tr>\n"; 01129 return $r; 01130 } 01131 01132 function _start_block( $header ) { 01133 global $wgOut; 01134 $wgOut->addHTML( $header ); 01135 } 01136 01137 function _end_block() { 01138 } 01139 01140 function _lines( $lines, $prefix=' ', $color="white" ) { 01141 } 01142 01143 function addedLine( $line ) { 01144 return '<td>+</td><td class="diff-addedline">' . 01145 $line.'</td>'; 01146 } 01147 01148 function deletedLine( $line ) { 01149 return '<td>-</td><td class="diff-deletedline">' . 01150 $line.'</td>'; 01151 } 01152 01153 function emptyLine() { 01154 return '<td colspan="2">&nbsp;</td>'; 01155 } 01156 01157 function contextLine( $line ) { 01158 return '<td> </td><td class="diff-context">'.$line.'</td>'; 01159 } 01160 01161 function _added($lines) { 01162 global $wgOut; 01163 foreach ($lines as $line) { 01164 $wgOut->addHTML( '<tr>' . $this->emptyLine() . 01165 $this->addedLine( $line ) . "</tr>\n" ); 01166 } 01167 } 01168 01169 function _deleted($lines) { 01170 global $wgOut; 01171 foreach ($lines as $line) { 01172 $wgOut->addHTML( '<tr>' . $this->deletedLine( $line ) . 01173 $this->emptyLine() . "</tr>\n" ); 01174 } 01175 } 01176 01177 function _context( $lines ) { 01178 global $wgOut; 01179 foreach ($lines as $line) { 01180 $wgOut->addHTML( '<tr>' . $this->contextLine( $line ) . 01181 $this->contextLine( $line ) . "</tr>\n" ); 01182 } 01183 } 01184 01185 function _changed( $orig, $closing ) { 01186 global $wgOut; 01187 $diff = new WordLevelDiff( $orig, $closing ); 01188 $del = $diff->orig(); 01189 $add = $diff->closing(); 01190 01191 while ( $line = array_shift( $del ) ) { 01192 $aline = array_shift( $add ); 01193 $wgOut->addHTML( '<tr>' . $this->deletedLine( $line ) . 01194 $this->addedLine( $aline ) . "</tr>\n" ); 01195 } 01196 $this->_added( $add ); # If any leftovers 01197 } 01198 } 01199 01200 ?>

Generated on Tue Jun 29 23:40:03 2004 for Mediawiki by doxygen 1.3.7