39 $this->mTitle = $titleObj;
40 wfDebug(
"DifferenceEngine old '$old' new '$new' rcid '$rcid'\n");
42 if (
'prev' === $new ) {
43 # Show diff between revision $old and the previous one.
44 # Get previous one from DB.
46 $this->mNewid = intval($old);
48 $this->mOldid = $this->mTitle->getPreviousRevisionID( $this->mNewid );
50 } elseif (
'next' === $new ) {
51 # Show diff between revision $old and the previous one.
52 # Get previous one from DB.
54 $this->mOldid = intval($old);
55 $this->mNewid = $this->mTitle->getNextRevisionID( $this->mOldid );
56 if (
false === $this->mNewid ) {
57 # if no result, NewId points to the newest old revision. The only newer
58 # revision is cur, which is "0".
63 $this->mOldid = intval($old);
64 $this->mNewid = intval($new);
66 $this->mRcidMarkPatrolled = intval($rcid); # force it to be an integer
70 global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol;
71 $fname =
'DifferenceEngine::showDiffPage';
74 # If external diffs are enabled both globally and for the user,
75 # we'll use the application/x-external-editor interface to call
76 # an external diff tool like kompare, kdiff3, etc.
77 if($wgUseExternalEditor && $wgUser->getOption(
'externaldiff')) {
78 global $wgInputEncoding,$wgServer,$wgScript,$wgLang;
80 header (
"Content-type: application/x-external-editor; charset=".$wgInputEncoding );
81 $url1=$this->mTitle->getFullURL(
"action=raw&oldid=".$this->mOldid);
82 $url2=$this->mTitle->getFullURL(
"action=raw&oldid=".$this->mNewid);
83 $special=$wgLang->getNsText(NS_SPECIAL);
88 Script={$wgServer}{$wgScript}
89 Special
namespace={$special}
103 $wgOut->setArticleFlag(
false );
105 $t = $this->mTitle->getPrefixedText() .
" (Diff: {$this->mOldid}, {$this->mNewid})";
106 $mtext =
wfMsg(
'missingarticle',
"<nowiki>$t</nowiki>" );
107 $wgOut->setPagetitle(
wfMsg(
'errorpagetitle' ) );
108 $wgOut->addWikitext( $mtext );
113 wfRunHooks(
'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) );
115 if ( $this->mNewRev->isCurrent() ) {
116 $wgOut->setArticleFlag(
true );
119 # mOldid is false if the difference engine is called with a "vague" query for
120 # a diff between a version V and its previous version V' AND the version V
121 # is the first version of that article. In that case, V' does not exist.
122 if ( $this->mOldid ===
false ) {
129 $wgOut->suppressQuickbar();
131 $oldTitle = $this->mOldPage->getPrefixedText();
132 $newTitle = $this->mNewPage->getPrefixedText();
133 if( $oldTitle == $newTitle ) {
134 $wgOut->setPageTitle( $newTitle );
136 $wgOut->setPageTitle( $oldTitle .
', ' . $newTitle );
138 $wgOut->setSubtitle(
wfMsg(
'difference' ) );
139 $wgOut->setRobotpolicy(
'noindex,nofollow' );
141 if ( !( $this->mOldPage->userCanRead() && $this->mNewPage->userCanRead() ) ) {
142 $wgOut->loginToUse();
148 $sk = $wgUser->getSkin();
150 if ( $this->mNewRev->isCurrent() && $wgUser->isAllowed(
'rollback') ) {
151 $rollback =
' ' . $sk->generateRollback( $this->mNewRev );
155 if( $wgUseRCPatrol && $this->mRcidMarkPatrolled != 0 && $wgUser->isAllowed(
'patrol' ) ) {
156 $patrol =
' [' . $sk->makeKnownLinkObj( $this->mTitle,
wfMsg(
'markaspatrolleddiff' ),
"action=markpatrolled&rcid={$this->mRcidMarkPatrolled}" ) .
']';
161 $prevlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml(
'previousdiff' ),
162 'diff=prev&oldid='.$this->mOldid,
'',
'',
'id="differences-prevlink"' );
163 if ( $this->mNewRev->isCurrent() ) {
164 $nextlink =
' ';
166 $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml(
'nextdiff' ),
167 'diff=next&oldid='.$this->mNewid,
'',
'',
'id="differences-nextlink"' );
173 if ($this->mOldRev->mMinorEdit == 1) {
174 $oldminor = wfElement(
'span', array(
'class' =>
'minor' ),
175 wfMsg(
'minoreditletter') ) .
' ';
178 if ($this->mNewRev->mMinorEdit == 1) {
179 $newminor = wfElement(
'span', array(
'class' =>
'minor' ),
180 wfMsg(
'minoreditletter') ) .
' ';
183 $oldHeader =
"<strong>{$this->mOldtitle}</strong><br />" .
184 $sk->revUserTools( $this->mOldRev ) .
"<br />" .
185 $oldminor . $sk->revComment( $this->mOldRev, !$diffOnly ) .
"<br />" .
187 $newHeader =
"<strong>{$this->mNewtitle}</strong><br />" .
188 $sk->revUserTools( $this->mNewRev ) .
" $rollback<br />" .
189 $newminor . $sk->revComment( $this->mNewRev, !$diffOnly ) .
"<br />" .
192 $this->
showDiff( $oldHeader, $newHeader );
205 $fname =
'DifferenceEngine::renderNewRevision';
208 $wgOut->addHTML(
"<hr /><h2>{$this->mPagetitle}</h2>\n" );
209 #add deleted rev tag if needed
210 if ( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
211 $wgOut->addWikiText(
wfMsg(
'rev-deleted-text-permission' ) );
214 if( !$this->mNewRev->isCurrent() ) {
215 $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection(
false );
219 if( is_object( $this->mNewRev ) ) {
220 $wgOut->setRevisionId( $this->mNewRev->getId() );
223 $wgOut->addWikiTextTidy( $this->mNewtext );
225 if( !$this->mNewRev->isCurrent() ) {
226 $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting );
237 global $wgOut, $wgUser;
239 $fname =
'DifferenceEngine::showFirstRevision';
242 # Get article text from the DB
245 $t = $this->mTitle->getPrefixedText() .
" (Diff: {$this->mOldid}, " .
247 $mtext =
wfMsg(
'missingarticle',
"<nowiki>$t</nowiki>" );
248 $wgOut->setPagetitle(
wfMsg(
'errorpagetitle' ) );
249 $wgOut->addWikitext( $mtext );
253 if ( $this->mNewRev->isCurrent() ) {
254 $wgOut->setArticleFlag(
true );
257 # Check if user is allowed to look at this page. If not, bail out.
259 if ( !( $this->mTitle->userCanRead() ) ) {
260 $wgOut->loginToUse();
266 # Prepare the header box
268 $sk = $wgUser->getSkin();
270 $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml(
'nextdiff' ),
'diff=next&oldid='.$this->mNewid,
'',
'',
'id="differences-nextlink"' );
271 $header =
"<div class=\"firstrevisionheader\" style=\"text-align: center\"><strong>{$this->mOldtitle}</strong><br />" .
272 $sk->revUserTools( $this->mNewRev ) .
"<br />" .
273 $sk->revComment( $this->mNewRev ) .
"<br />" .
274 $nextlink .
"</div>\n";
276 $wgOut->addHTML( $header );
278 $wgOut->setSubtitle(
wfMsg(
'difference' ) );
279 $wgOut->setRobotpolicy(
'noindex,nofollow' );
290 $diff = $this->
getDiff( $otitle, $ntitle );
291 if ( $diff ===
false ) {
292 $wgOut->addWikitext(
wfMsg(
'missingarticle',
"<nowiki>(fixme, bug)</nowiki>" ) );
295 $wgOut->addHTML( $diff );
307 if ( $body ===
false ) {
311 return $this->
addHeader( $body, $otitle, $ntitle, $multi );
322 $fname =
'DifferenceEngine::getDiffBody';
327 if ( $this->mOldid && $this->mNewid ) {
329 $key = wfMemcKey(
'diff',
'oldid', $this->mOldid,
'newid', $this->mNewid );
330 $difftext = $wgMemc->get( $key );
332 wfIncrStats(
'diff_cache_hit' );
334 $difftext .=
"\n<!-- diff cache key $key -->\n";
340 #loadtext is permission safe, this just clears out the diff
344 }
else if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) {
346 }
else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
353 if ( $key !==
false && $difftext !==
false ) {
354 wfIncrStats(
'diff_cache_miss' );
355 $wgMemc->set( $key, $difftext, 7*86400 );
357 wfIncrStats(
'diff_uncacheable' );
360 if ( $difftext !==
false ) {
372 global $wgExternalDiffEngine, $wgContLang;
373 $fname =
'DifferenceEngine::generateDiffBody';
375 $otext = str_replace(
"\r\n",
"\n", $otext );
376 $ntext = str_replace(
"\r\n",
"\n", $ntext );
378 if ( $wgExternalDiffEngine ==
'wikidiff' ) {
379 # For historical reasons, external diff engine expects
380 # input text to be HTML-escaped already
381 $otext = htmlspecialchars ( $wgContLang->segmentForDiff( $otext ) );
382 $ntext = htmlspecialchars ( $wgContLang->segmentForDiff( $ntext ) );
383 if( !function_exists(
'wikidiff_do_diff' ) ) {
384 dl(
'php_wikidiff.so');
386 return $wgContLang->unsegementForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) );
389 if ( $wgExternalDiffEngine ==
'wikidiff2' ) {
390 # Better external diff engine, the 2 may some day be dropped
391 # This one does the escaping and segmenting itself
392 if ( !function_exists(
'wikidiff2_do_diff' ) ) {
394 @dl(
'php_wikidiff2.so');
397 if ( function_exists(
'wikidiff2_do_diff' ) ) {
399 $text = wikidiff2_do_diff( $otext, $ntext, 2 );
404 if ( $wgExternalDiffEngine !==
false ) {
406 global $wgTmpDirectory;
407 $tempName1 = tempnam( $wgTmpDirectory,
'diff_' );
408 $tempName2 = tempnam( $wgTmpDirectory,
'diff_' );
410 $tempFile1 = fopen( $tempName1,
"w" );
415 $tempFile2 = fopen( $tempName2,
"w" );
420 fwrite( $tempFile1, $otext );
421 fwrite( $tempFile2, $ntext );
422 fclose( $tempFile1 );
423 fclose( $tempFile2 );
424 $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 );
426 $difftext = wfShellExec(
$cmd );
428 unlink( $tempName1 );
429 unlink( $tempName2 );
434 $ota = explode(
"\n", $wgContLang->segmentForDiff( $otext ) );
435 $nta = explode(
"\n", $wgContLang->segmentForDiff( $ntext ) );
436 $diffs =
new Diff( $ota, $nta );
438 return $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) );
446 return preg_replace_callback(
'/<!--LINE (\d+)-->/',
447 array( &$this,
'localiseLineNumbersCb' ), $text );
452 return wfMsgExt(
'lineno', array(
'parseinline'), $wgLang->formatNum( $matches[1] ) );
460 if ( !is_object($this->mOldRev) || !is_object($this->mNewRev) )
463 if( !$this->mOldPage->equals( $this->mNewPage ) ) {
468 $oldid = $this->mOldRev->getId();
469 $newid = $this->mNewRev->getId();
470 if ( $oldid > $newid ) {
471 $tmp = $oldid; $oldid = $newid; $newid = $tmp;
474 $n = $this->mTitle->countRevisionsBetween( $oldid, $newid );
478 return wfMsgExt(
'diff-multi', array(
'parseinline' ),
$n );
485 function addHeader( $diff, $otitle, $ntitle, $multi =
'' ) {
488 if ( $this->mOldRev && $this->mOldRev->isDeleted(Revision::DELETED_TEXT) ) {
489 $otitle =
'<span class="history-deleted">'.$otitle.
'</span>';
491 if ( $this->mNewRev && $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
492 $ntitle =
'<span class="history-deleted">'.$ntitle.
'</span>';
495 <table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'>
497 <td colspan='2' width='50%' align='center' class='diff-otitle'>{$otitle}</td>
498 <td colspan='2' width='50%' align='center' class='diff-ntitle'>{$ntitle}</td>
503 $header .=
"<tr><td colspan='4' align='center' class='diff-multi'>{$multi}</td></tr>";
505 return $header . $diff .
"</table>";
512 $this->mOldtext = $oldText;
513 $this->mNewtext = $newText;
514 $this->mTextLoaded = 2;
529 if ( $this->mRevisionsLoaded ) {
533 $this->mRevisionsLoaded =
true;
537 if( $this->mNewid ) {
538 $this->mNewRev = Revision::newFromId( $this->mNewid );
540 $this->mNewRev = Revision::newFromTitle( $this->mTitle );
543 if( is_null( $this->mNewRev ) ) {
548 $timestamp = $wgLang->timeanddate( $this->mNewRev->getTimestamp(), true );
549 $this->mNewPage = $this->mNewRev->getTitle();
550 if( $this->mNewRev->isCurrent() ) {
551 $newLink = $this->mNewPage->escapeLocalUrl();
552 $this->mPagetitle = htmlspecialchars(
wfMsg(
'currentrev' ) );
553 $newEdit = $this->mNewPage->escapeLocalUrl(
'action=edit' );
555 $this->mNewtitle =
"<a href='$newLink'>{$this->mPagetitle}</a> ($timestamp)"
556 .
" (<a href='$newEdit'>" . htmlspecialchars(
wfMsg(
'editold' ) ) .
"</a>)";
559 $newLink = $this->mNewPage->escapeLocalUrl(
'oldid=' . $this->mNewid );
560 $newEdit = $this->mNewPage->escapeLocalUrl(
'action=edit&oldid=' . $this->mNewid );
561 $this->mPagetitle = htmlspecialchars(
wfMsg(
'revisionasof',
$timestamp ) );
563 $this->mNewtitle =
"<a href='$newLink'>{$this->mPagetitle}</a>"
564 .
" (<a href='$newEdit'>" . htmlspecialchars(
wfMsg(
'editold' ) ) .
"</a>)";
568 $this->mOldRev =
false;
569 if( $this->mOldid ) {
570 $this->mOldRev = Revision::newFromId( $this->mOldid );
571 } elseif ( $this->mOldid === 0 ) {
572 $rev = $this->mNewRev->getPrevious();
574 $this->mOldid = $rev->getId();
575 $this->mOldRev = $rev;
578 $this->mOldid =
false;
579 $this->mOldRev =
false;
583 if( is_null( $this->mOldRev ) ) {
587 if ( $this->mOldRev ) {
588 $this->mOldPage = $this->mOldRev->getTitle();
590 $t = $wgLang->timeanddate( $this->mOldRev->getTimestamp(), true );
591 $oldLink = $this->mOldPage->escapeLocalUrl(
'oldid=' . $this->mOldid );
592 $oldEdit = $this->mOldPage->escapeLocalUrl(
'action=edit&oldid=' . $this->mOldid );
593 $this->mOldtitle =
"<a href='$oldLink'>" . htmlspecialchars(
wfMsg(
'revisionasof',
$t ) )
594 .
"</a> (<a href='$oldEdit'>" . htmlspecialchars(
wfMsg(
'editold' ) ) .
"</a>)";
596 $newUndo = $this->mNewPage->escapeLocalUrl(
'action=edit&undoafter=' . $this->mOldid .
'&undo=' . $this->mNewid);
597 $this->mNewtitle .=
" (<a href='$newUndo'>" . htmlspecialchars(
wfMsg(
'editundo' ) ) .
"</a>)";
607 if ( $this->mTextLoaded == 2 ) {
611 $this->mTextLoaded = 2;
617 if ( $this->mOldRev ) {
619 $this->mOldtext = $this->mOldRev->revText();
620 if ( $this->mOldtext ===
false ) {
624 if ( $this->mNewRev ) {
625 $this->mNewtext = $this->mNewRev->revText();
626 if ( $this->mNewtext ===
false ) {
637 if ( $this->mTextLoaded >= 1 ) {
640 $this->mTextLoaded = 1;
645 $this->mNewtext = $this->mNewRev->getText();
658 define(
'USE_ASSERTS', function_exists(
'assert'));
671 trigger_error(
'pure virtual', E_USER_ERROR);
709 var
$type =
'delete';
712 $this->orig = $lines;
713 $this->closing =
false;
730 $this->closing = $lines;
745 var
$type =
'change';
785 function diff ($from_lines, $to_lines) {
786 $fname =
'_DiffEngine::diff';
789 $n_from =
sizeof($from_lines);
790 $n_to =
sizeof($to_lines);
792 $this->xchanged = $this->ychanged = array();
793 $this->xv = $this->yv = array();
794 $this->xind = $this->yind = array();
796 unset($this->in_seq);
800 for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
801 if ($from_lines[$skip] !== $to_lines[$skip])
803 $this->xchanged[$skip] = $this->ychanged[$skip] =
false;
806 $xi = $n_from; $yi = $n_to;
807 for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
808 if ($from_lines[$xi] !== $to_lines[$yi])
810 $this->xchanged[$xi] = $this->ychanged[$yi] =
false;
814 for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
815 $xhash[$this->
_line_hash($from_lines[$xi])] = 1;
818 for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
819 $line = $to_lines[$yi];
820 if ( ($this->ychanged[$yi] = empty($xhash[$this->
_line_hash($line)])) )
826 for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
827 $line = $from_lines[$xi];
828 if ( ($this->xchanged[$xi] = empty($yhash[$this->
_line_hash($line)])) )
835 $this->
_compareseq(0,
sizeof($this->xv), 0,
sizeof($this->yv));
844 while ($xi < $n_from || $yi < $n_to) {
845 USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
846 USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
850 while ( $xi < $n_from && $yi < $n_to
851 && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
852 $copy[] = $from_lines[$xi++];
860 while ($xi < $n_from && $this->xchanged[$xi])
861 $delete[] = $from_lines[$xi++];
864 while ($yi < $n_to && $this->ychanged[$yi])
865 $add[] = $to_lines[$yi++];
882 if ( strlen( $line ) > self::MAX_XREF_LENGTH ) {
906 function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks) {
907 $fname =
'_DiffEngine::_diag';
911 if ($xlim - $xoff > $ylim - $yoff) {
915 list ($xoff, $xlim, $yoff, $ylim)
916 = array( $yoff, $ylim, $xoff, $xlim);
920 for ($i = $ylim - 1; $i >= $yoff; $i--)
921 $ymatches[$this->xv[$i]][] = $i;
923 for ($i = $ylim - 1; $i >= $yoff; $i--)
924 $ymatches[$this->yv[$i]][] = $i;
927 $this->seq[0]= $yoff - 1;
928 $this->in_seq = array();
931 $numer = $xlim - $xoff + $nchunks - 1;
933 for ($chunk = 0; $chunk < $nchunks; $chunk++) {
936 for ($i = 0; $i <= $this->lcs; $i++)
937 $ymids[$i][$chunk-1] = $this->seq[$i];
939 $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
940 for ( ; $x < $x1; $x++) {
941 $line = $flip ? $this->yv[$x] : $this->xv[$x];
942 if (empty($ymatches[$line]))
944 $matches = $ymatches[$line];
946 while (list ($junk, $y) = each($matches))
947 if (empty($this->in_seq[$y])) {
950 $ymids[$k] = $ymids[$k-1];
953 while (list ( , $y) = each($matches)) {
954 if ($y > $this->seq[$k-1]) {
958 $this->in_seq[$this->seq[$k]] =
false;
960 $this->in_seq[$y] = 1;
961 }
else if (empty($this->in_seq[$y])) {
964 $ymids[$k] = $ymids[$k-1];
971 $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
972 $ymid = $ymids[$this->lcs];
973 for (
$n = 0;
$n < $nchunks - 1;
$n++) {
974 $x1 = $xoff + (int)(($numer + ($xlim - $xoff) *
$n) / $nchunks);
976 $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
978 $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
981 return array($this->lcs, $seps);
985 $fname =
'_DiffEngine::_lcs_pos';
989 if ($end == 0 || $ypos > $this->seq[$end]) {
990 $this->seq[++$this->lcs] = $ypos;
991 $this->in_seq[$ypos] = 1;
997 while ($beg < $end) {
998 $mid = (int)(($beg + $end) / 2);
999 if ( $ypos > $this->seq[$mid] )
1007 $this->in_seq[$this->seq[$end]] =
false;
1008 $this->seq[$end] = $ypos;
1009 $this->in_seq[$ypos] = 1;
1025 function _compareseq ($xoff, $xlim, $yoff, $ylim) {
1026 $fname =
'_DiffEngine::_compareseq';
1030 while ($xoff < $xlim && $yoff < $ylim
1031 && $this->xv[$xoff] == $this->yv[$yoff]) {
1037 while ($xlim > $xoff && $ylim > $yoff
1038 && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
1043 if ($xoff == $xlim || $yoff == $ylim)
1049 $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
1051 = $this->
_diag($xoff,$xlim,$yoff, $ylim,$nchunks);
1057 while ($yoff < $ylim)
1058 $this->ychanged[$this->yind[$yoff++]] = 1;
1059 while ($xoff < $xlim)
1060 $this->xchanged[$this->xind[$xoff++]] = 1;
1065 while ($pt2 = next($seps)) {
1066 $this->
_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
1086 $fname =
'_DiffEngine::_shift_boundaries';
1091 USE_ASSERTS && assert(
'sizeof($lines) == sizeof($changed)');
1092 $len =
sizeof($lines);
1093 $other_len =
sizeof($other_changed);
1107 while ($j < $other_len && $other_changed[$j])
1110 while ($i < $len && !
$changed[$i]) {
1111 USE_ASSERTS && assert(
'$j < $other_len && ! $other_changed[$j]');
1113 while ($j < $other_len && $other_changed[$j])
1123 while (++$i < $len &&
$changed[$i])
1131 $runlength = $i - $start;
1138 while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
1141 while ($start > 0 &&
$changed[$start - 1])
1144 while ($other_changed[--$j])
1146 USE_ASSERTS && assert(
'$j >= 0 && !$other_changed[$j]');
1154 $corresponding = $j < $other_len ? $i : $len;
1163 while ($i < $len && $lines[$start] == $lines[$i]) {
1169 USE_ASSERTS && assert(
'$j < $other_len && ! $other_changed[$j]');
1171 if ($j < $other_len && $other_changed[$j]) {
1172 $corresponding = $i;
1173 while ($j < $other_len && $other_changed[$j])
1177 }
while ($runlength != $i - $start);
1183 while ($corresponding < $i) {
1187 while ($other_changed[--$j])
1189 USE_ASSERTS && assert(
'$j >= 0 && !$other_changed[$j]');
1214 function Diff($from_lines, $to_lines) {
1216 $this->edits = $eng->
diff($from_lines, $to_lines);
1230 function reverse () {
1232 $rev->edits = array();
1233 foreach ($this->edits as $edit) {
1234 $rev->edits[] = $edit->reverse();
1244 function isEmpty () {
1245 foreach ($this->edits as $edit) {
1246 if ($edit->type !=
'copy')
1261 foreach ($this->edits as $edit) {
1262 if ($edit->type ==
'copy')
1263 $lcs +=
sizeof($edit->orig);
1279 foreach ($this->edits as $edit) {
1281 array_splice($lines,
sizeof($lines), 0, $edit->orig);
1294 function closing() {
1297 foreach ($this->edits as $edit) {
1299 array_splice($lines,
sizeof($lines), 0, $edit->closing);
1309 function _check ($from_lines, $to_lines) {
1310 $fname =
'Diff::_check';
1312 if (serialize($from_lines) != serialize($this->orig()))
1313 trigger_error(
"Reconstructed original doesn't match", E_USER_ERROR);
1314 if (serialize($to_lines) != serialize($this->closing()))
1315 trigger_error(
"Reconstructed closing doesn't match", E_USER_ERROR);
1317 $rev = $this->reverse();
1318 if (serialize($to_lines) != serialize($rev->orig()))
1319 trigger_error(
"Reversed original doesn't match", E_USER_ERROR);
1320 if (serialize($from_lines) != serialize($rev->closing()))
1321 trigger_error(
"Reversed closing doesn't match", E_USER_ERROR);
1325 foreach ($this->edits as $edit) {
1326 if ( $prevtype == $edit->type )
1327 trigger_error(
"Edit sequence is non-optimal", E_USER_ERROR);
1328 $prevtype = $edit->type;
1331 $lcs = $this->lcs();
1332 trigger_error(
'Diff okay: LCS = '.$lcs, E_USER_NOTICE);
1368 $mapped_from_lines, $mapped_to_lines) {
1369 $fname =
'MappedDiff::MappedDiff';
1372 assert(
sizeof($from_lines) ==
sizeof($mapped_from_lines));
1373 assert(
sizeof($to_lines) ==
sizeof($mapped_to_lines));
1375 $this->
Diff($mapped_from_lines, $mapped_to_lines);
1378 for ($i = 0; $i <
sizeof($this->edits); $i++) {
1379 $orig = &$this->edits[$i]->orig;
1380 if (is_array($orig)) {
1381 $orig = array_slice($from_lines, $xi,
sizeof($orig));
1382 $xi +=
sizeof($orig);
1385 $closing = &$this->edits[$i]->closing;
1386 if (is_array($closing)) {
1387 $closing = array_slice($to_lines, $yi,
sizeof($closing));
1388 $yi +=
sizeof($closing);
1413 var $leading_context_lines = 0;
1421 var $trailing_context_lines = 0;
1429 function format($diff) {
1430 $fname =
'DiffFormatter::format';
1437 $nlead = $this->leading_context_lines;
1438 $ntrail = $this->trailing_context_lines;
1440 $this->_start_diff();
1442 foreach ($diff->edits as $edit) {
1443 if ($edit->type ==
'copy') {
1444 if (is_array($block)) {
1445 if (
sizeof($edit->orig) <= $nlead + $ntrail) {
1450 $context = array_slice($edit->orig, 0, $ntrail);
1453 $this->_block($x0, $ntrail + $xi - $x0,
1454 $y0, $ntrail + $yi - $y0,
1459 $context = $edit->orig;
1462 if (! is_array($block)) {
1463 $context = array_slice($context,
sizeof($context) - $nlead);
1464 $x0 = $xi -
sizeof($context);
1465 $y0 = $yi -
sizeof($context);
1474 $xi +=
sizeof($edit->orig);
1476 $yi +=
sizeof($edit->closing);
1479 if (is_array($block))
1480 $this->_block($x0, $xi - $x0,
1484 $end = $this->_end_diff();
1489 function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
1490 $fname =
'DiffFormatter::_block';
1492 $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
1493 foreach ($edits as $edit) {
1494 if ($edit->type ==
'copy')
1495 $this->_context($edit->orig);
1496 elseif ($edit->type ==
'add')
1497 $this->_added($edit->closing);
1498 elseif ($edit->type == 'delete')
1499 $this->_deleted($edit->orig);
1500 elseif ($edit->type == 'change')
1501 $this->_changed($edit->orig, $edit->closing);
1503 trigger_error('Unknown edit type', E_USER_ERROR);
1505 $this->_end_block();
1509 function _start_diff() {
1513 function _end_diff() {
1514 $val = ob_get_contents();
1519 function _block_header($xbeg, $xlen, $ybeg, $ylen) {
1521 $xbeg .=
"," . ($xbeg + $xlen - 1);
1523 $ybeg .=
"," . ($ybeg + $ylen - 1);
1525 return $xbeg . ($xlen ? ($ylen ?
'c' :
'd') :
'a') . $ybeg;
1528 function _start_block($header) {
1532 function _end_block() {
1535 function _lines($lines, $prefix =
' ') {
1536 foreach ($lines as $line)
1537 echo
"$prefix $line\n";
1540 function _context($lines) {
1541 $this->_lines($lines);
1544 function _added($lines) {
1545 $this->_lines($lines,
'>');
1547 function _deleted($lines) {
1548 $this->_lines($lines,
'<');
1551 function _changed($orig, $closing) {
1552 $this->_deleted($orig);
1554 $this->_added($closing);
1564 define(
'NBSP',
' ');
1573 $this->_lines = array();
1579 function _flushGroup ($new_tag) {
1580 if ($this->_group !==
'') {
1581 if ($this->_tag ==
'ins')
1582 $this->_line .=
'[ilDiffInsStart]' .
1583 htmlspecialchars ( $this->_group ) .
'[ilDiffInsEnd]';
1584 elseif ($this->_tag ==
'del')
1585 $this->_line .=
'[ilDiffDelStart]' .
1586 htmlspecialchars ( $this->_group ) .
'[ilDiffDelEnd]';
1588 $this->_line .= htmlspecialchars ( $this->_group );
1591 $this->_tag = $new_tag;
1594 function _flushLine ($new_tag) {
1595 $this->_flushGroup($new_tag);
1596 if ($this->_line !=
'')
1597 array_push ( $this->_lines, $this->_line );
1599 # make empty lines visible by inserting an NBSP
1600 array_push ( $this->_lines,
NBSP );
1604 function addWords ($words, $tag =
'') {
1605 if ($tag != $this->_tag)
1606 $this->_flushGroup($tag);
1608 foreach ($words as $word) {
1612 if ($word[0] ==
"\n") {
1613 $this->_flushLine($tag);
1614 $word = substr($word, 1);
1616 assert(!strstr($word,
"\n"));
1617 $this->_group .= $word;
1621 function getLines() {
1622 $this->_flushLine(
'~done');
1623 return $this->_lines;
1634 const MAX_LINE_LENGTH = 10000;
1637 $fname =
'WordLevelDiff::WordLevelDiff';
1640 list ($orig_words, $orig_stripped) = $this->_split($orig_lines);
1641 list ($closing_words, $closing_stripped) = $this->_split($closing_lines);
1643 $this->
MappedDiff($orig_words, $closing_words,
1644 $orig_stripped, $closing_stripped);
1648 function _split($lines) {
1649 $fname =
'WordLevelDiff::_split';
1653 $stripped = array();
1655 foreach ( $lines as $line ) {
1656 # If the line is too long, just pretend the entire line is one big word
1657 # This prevents resource exhaustion problems
1664 if ( strlen( $line ) > self::MAX_LINE_LENGTH ) {
1666 $stripped[] = $line;
1669 if (preg_match_all(
'/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs',
1672 $words = array_merge( $words, $m[0] );
1673 $stripped = array_merge( $stripped, $m[1] );
1678 return array($words, $stripped);
1682 $fname =
'WordLevelDiff::orig';
1686 foreach ($this->edits as $edit) {
1687 if ($edit->type ==
'copy')
1689 elseif ($edit->orig)
1690 $orig->addWords($edit->orig,
'del');
1692 $lines = $orig->getLines();
1697 function closing () {
1698 $fname =
'WordLevelDiff::closing';
1702 foreach ($this->edits as $edit) {
1703 if ($edit->type ==
'copy')
1704 $closing->
addWords($edit->closing);
1705 elseif ($edit->closing)
1706 $closing->addWords($edit->closing, 'ins');
1708 $lines = $closing->getLines();
1723 $this->leading_context_lines = 2;
1724 $this->trailing_context_lines = 2;
1727 function _block_header( $xbeg, $xlen, $ybeg, $ylen ) {
1728 $r =
'<tr><td colspan="2" align="left"><strong><!--LINE '.$xbeg.
"--></strong></td>\n" .
1729 '<td colspan="2" align="left"><strong><!--LINE '.$ybeg.
"--></strong></td></tr>\n";
1733 function _start_block( $header ) {
1737 function _end_block() {
1740 function _lines( $lines, $prefix=
' ', $color=
'white' ) {
1743 # HTML-escape parameter before calling this
1744 function addedLine( $line ) {
1745 return "<td>+</td><td class='diff-addedline'>{$line}</td>";
1748 # HTML-escape parameter before calling this
1749 function deletedLine( $line ) {
1750 return "<td>-</td><td class='diff-deletedline'>{$line}</td>";
1753 # HTML-escape parameter before calling this
1754 function contextLine( $line ) {
1755 return "<td> </td><td class='diff-context'>{$line}</td>";
1758 function emptyLine() {
1759 return '<td colspan="2"> </td>';
1762 function _added( $lines ) {
1763 foreach ($lines as $line) {
1764 echo
'<tr>' . $this->emptyLine() .
1765 $this->addedLine( htmlspecialchars ( $line ) ) .
"</tr>\n";
1769 function _deleted($lines) {
1770 foreach ($lines as $line) {
1771 echo
'<tr>' . $this->deletedLine( htmlspecialchars ( $line ) ) .
1772 $this->emptyLine() .
"</tr>\n";
1776 function _context( $lines ) {
1777 foreach ($lines as $line) {
1779 $this->contextLine( htmlspecialchars ( $line ) ) .
1780 $this->contextLine( htmlspecialchars ( $line ) ) .
"</tr>\n";
1784 function _changed( $orig, $closing ) {
1785 $fname =
'TableDiffFormatter::_changed';
1789 $del = $diff->orig();
1790 $add = $diff->closing();
1792 # Notice that WordLevelDiff returns HTML-escaped output.
1793 # Hence, we will be calling addedLine/deletedLine without HTML-escaping.
1795 while ( $line = array_shift( $del ) ) {
1796 $aline = array_shift( $add );
1797 echo
'<tr>' . $this->deletedLine( $line ) .
1798 $this->addedLine( $aline ) .
"</tr>\n";
1800 foreach ($add as $line) { # If any leftovers
1801 echo
'<tr>' . $this->emptyLine() .
1802 $this->addedLine( $line ) .
"</tr>\n";