ILIAS  release_5-1 Revision 5.0.0-5477-g43f3e3fab5f
class.WordLevelDiff.php
Go to the documentation of this file.
1<?php
2
3// Copyright holded by MediaWiki contributers, Licensed under GPL version 2 or later
4
5
27 var $mRevisionsLoaded = false; // Have the revisions been loaded
28 var $mTextLoaded = 0; // How many text blobs have been loaded, 0, 1 or 2?
38 function DifferenceEngine( $titleObj = null, $old = 0, $new = 0, $rcid = 0 ) {
39 $this->mTitle = $titleObj;
40 wfDebug("DifferenceEngine old '$old' new '$new' rcid '$rcid'\n");
41
42 if ( 'prev' === $new ) {
43 # Show diff between revision $old and the previous one.
44 # Get previous one from DB.
45 #
46 $this->mNewid = intval($old);
47
48 $this->mOldid = $this->mTitle->getPreviousRevisionID( $this->mNewid );
49
50 } elseif ( 'next' === $new ) {
51 # Show diff between revision $old and the previous one.
52 # Get previous one from DB.
53 #
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".
59 $this->mNewid = 0;
60 }
61
62 } else {
63 $this->mOldid = intval($old);
64 $this->mNewid = intval($new);
65 }
66 $this->mRcidMarkPatrolled = intval($rcid); # force it to be an integer
67 }
68
69 function showDiffPage( $diffOnly = false ) {
70 global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol;
71 $fname = 'DifferenceEngine::showDiffPage';
72 //wfProfileIn( $fname );
73
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;
79 $wgOut->disable();
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);
84 $control=<<<CONTROL
85[Process]
86Type=Diff text
87Engine=MediaWiki
88Script={$wgServer}{$wgScript}
89Special namespace={$special}
90
91[File]
92Extension=wiki
93URL=$url1
94
95[File 2]
96Extension=wiki
97URL=$url2
98CONTROL;
99 echo($control);
100 return;
101 }
102
103 $wgOut->setArticleFlag( false );
104 if ( ! $this->loadRevisionData() ) {
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 );
109 //wfProfileOut( $fname );
110 return;
111 }
112
113 wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) );
114
115 if ( $this->mNewRev->isCurrent() ) {
116 $wgOut->setArticleFlag( true );
117 }
118
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 ) {
123 $this->showFirstRevision();
124 $this->renderNewRevision(); // should we respect $diffOnly here or not?
125 //wfProfileOut( $fname );
126 return;
127 }
128
129 $wgOut->suppressQuickbar();
130
131 $oldTitle = $this->mOldPage->getPrefixedText();
132 $newTitle = $this->mNewPage->getPrefixedText();
133 if( $oldTitle == $newTitle ) {
134 $wgOut->setPageTitle( $newTitle );
135 } else {
136 $wgOut->setPageTitle( $oldTitle . ', ' . $newTitle );
137 }
138 $wgOut->setSubtitle( wfMsg( 'difference' ) );
139 $wgOut->setRobotpolicy( 'noindex,nofollow' );
140
141 if ( !( $this->mOldPage->userCanRead() && $this->mNewPage->userCanRead() ) ) {
142 $wgOut->loginToUse();
143 $wgOut->output();
144 //wfProfileOut( $fname );
145 exit;
146 }
147
148 $sk = $wgUser->getSkin();
149
150 if ( $this->mNewRev->isCurrent() && $wgUser->isAllowed('rollback') ) {
151 $rollback = '&nbsp;&nbsp;&nbsp;' . $sk->generateRollback( $this->mNewRev );
152 } else {
153 $rollback = '';
154 }
155 if( $wgUseRCPatrol && $this->mRcidMarkPatrolled != 0 && $wgUser->isAllowed( 'patrol' ) ) {
156 $patrol = ' [' . $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'markaspatrolleddiff' ), "action=markpatrolled&rcid={$this->mRcidMarkPatrolled}" ) . ']';
157 } else {
158 $patrol = '';
159 }
160
161 $prevlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'previousdiff' ),
162 'diff=prev&oldid='.$this->mOldid, '', '', 'id="differences-prevlink"' );
163 if ( $this->mNewRev->isCurrent() ) {
164 $nextlink = '&nbsp;';
165 } else {
166 $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ),
167 'diff=next&oldid='.$this->mNewid, '', '', 'id="differences-nextlink"' );
168 }
169
170 $oldminor = '';
171 $newminor = '';
172
173 if ($this->mOldRev->mMinorEdit == 1) {
174 $oldminor = wfElement( 'span', array( 'class' => 'minor' ),
175 wfMsg( 'minoreditletter') ) . ' ';
176 }
177
178 if ($this->mNewRev->mMinorEdit == 1) {
179 $newminor = wfElement( 'span', array( 'class' => 'minor' ),
180 wfMsg( 'minoreditletter') ) . ' ';
181 }
182
183 $oldHeader = "<strong>{$this->mOldtitle}</strong><br />" .
184 $sk->revUserTools( $this->mOldRev ) . "<br />" .
185 $oldminor . $sk->revComment( $this->mOldRev, !$diffOnly ) . "<br />" .
186 $prevlink;
187 $newHeader = "<strong>{$this->mNewtitle}</strong><br />" .
188 $sk->revUserTools( $this->mNewRev ) . " $rollback<br />" .
189 $newminor . $sk->revComment( $this->mNewRev, !$diffOnly ) . "<br />" .
190 $nextlink . $patrol;
191
192 $this->showDiff( $oldHeader, $newHeader );
193
194 if ( !$diffOnly )
195 $this->renderNewRevision();
196
197 //wfProfileOut( $fname );
198 }
199
203 function renderNewRevision() {
204 global $wgOut;
205 $fname = 'DifferenceEngine::renderNewRevision';
206 //wfProfileIn( $fname );
207
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' ) );
212 }
213
214 if( !$this->mNewRev->isCurrent() ) {
215 $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
216 }
217
218 $this->loadNewText();
219 if( is_object( $this->mNewRev ) ) {
220 $wgOut->setRevisionId( $this->mNewRev->getId() );
221 }
222
223 $wgOut->addWikiTextTidy( $this->mNewtext );
224
225 if( !$this->mNewRev->isCurrent() ) {
226 $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting );
227 }
228
229 //wfProfileOut( $fname );
230 }
231
236 function showFirstRevision() {
237 global $wgOut, $wgUser;
238
239 $fname = 'DifferenceEngine::showFirstRevision';
240 //wfProfileIn( $fname );
241
242 # Get article text from the DB
243 #
244 if ( ! $this->loadNewText() ) {
245 $t = $this->mTitle->getPrefixedText() . " (Diff: {$this->mOldid}, " .
246 "{$this->mNewid})";
247 $mtext = wfMsg( 'missingarticle', "<nowiki>$t</nowiki>" );
248 $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
249 $wgOut->addWikitext( $mtext );
250 //wfProfileOut( $fname );
251 return;
252 }
253 if ( $this->mNewRev->isCurrent() ) {
254 $wgOut->setArticleFlag( true );
255 }
256
257 # Check if user is allowed to look at this page. If not, bail out.
258 #
259 if ( !( $this->mTitle->userCanRead() ) ) {
260 $wgOut->loginToUse();
261 $wgOut->output();
262 //wfProfileOut( $fname );
263 exit;
264 }
265
266 # Prepare the header box
267 #
268 $sk = $wgUser->getSkin();
269
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";
275
276 $wgOut->addHTML( $header );
277
278 $wgOut->setSubtitle( wfMsg( 'difference' ) );
279 $wgOut->setRobotpolicy( 'noindex,nofollow' );
280
281 //wfProfileOut( $fname );
282 }
283
288 function showDiff( $otitle, $ntitle ) {
289 global $wgOut;
290 $diff = $this->getDiff( $otitle, $ntitle );
291 if ( $diff === false ) {
292 $wgOut->addWikitext( wfMsg( 'missingarticle', "<nowiki>(fixme, bug)</nowiki>" ) );
293 return false;
294 } else {
295 $wgOut->addHTML( $diff );
296 return true;
297 }
298 }
299
305 function getDiff( $otitle, $ntitle ) {
306 $body = $this->getDiffBody();
307 if ( $body === false ) {
308 return false;
309 } else {
310 $multi = $this->getMultiNotice();
311 return $this->addHeader( $body, $otitle, $ntitle, $multi );
312 }
313 }
314
320 function getDiffBody() {
321 global $wgMemc;
322 $fname = 'DifferenceEngine::getDiffBody';
323 //wfProfileIn( $fname );
324
325 // Cacheable?
326 $key = false;
327 if ( $this->mOldid && $this->mNewid ) {
328 // Try cache
329 $key = wfMemcKey( 'diff', 'oldid', $this->mOldid, 'newid', $this->mNewid );
330 $difftext = $wgMemc->get( $key );
331 if ( $difftext ) {
332 wfIncrStats( 'diff_cache_hit' );
333 $difftext = $this->localiseLineNumbers( $difftext );
334 $difftext .= "\n<!-- diff cache key $key -->\n";
335 //wfProfileOut( $fname );
336 return $difftext;
337 }
338 }
339
340 #loadtext is permission safe, this just clears out the diff
341 if ( !$this->loadText() ) {
342 //wfProfileOut( $fname );
343 return false;
344 } else if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) {
345 return '';
346 } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
347 return '';
348 }
349
350 $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
351
352 // Save to cache for 7 days
353 if ( $key !== false && $difftext !== false ) {
354 wfIncrStats( 'diff_cache_miss' );
355 $wgMemc->set( $key, $difftext, 7*86400 );
356 } else {
357 wfIncrStats( 'diff_uncacheable' );
358 }
359 // Replace line numbers with the text in the user's language
360 if ( $difftext !== false ) {
361 $difftext = $this->localiseLineNumbers( $difftext );
362 }
363 //wfProfileOut( $fname );
364 return $difftext;
365 }
366
371 function generateDiffBody( $otext, $ntext ) {
372 global $wgExternalDiffEngine, $wgContLang;
373 $fname = 'DifferenceEngine::generateDiffBody';
374
375 $otext = str_replace( "\r\n", "\n", $otext );
376 $ntext = str_replace( "\r\n", "\n", $ntext );
377
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');
385 }
386 return $wgContLang->unsegementForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) );
387 }
388
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' ) ) {
393 //wfProfileIn( "$fname-dl" );
394 @dl('php_wikidiff2.so');
395 //wfProfileOut( "$fname-dl" );
396 }
397 if ( function_exists( 'wikidiff2_do_diff' ) ) {
398 //wfProfileIn( 'wikidiff2_do_diff' );
399 $text = wikidiff2_do_diff( $otext, $ntext, 2 );
400 //wfProfileOut( 'wikidiff2_do_diff' );
401 return $text;
402 }
403 }
404 if ( $wgExternalDiffEngine !== false ) {
405 # Diff via the shell
406 global $wgTmpDirectory;
407 $tempName1 = tempnam( $wgTmpDirectory, 'diff_' );
408 $tempName2 = tempnam( $wgTmpDirectory, 'diff_' );
409
410 $tempFile1 = fopen( $tempName1, "w" );
411 if ( !$tempFile1 ) {
412 //wfProfileOut( $fname );
413 return false;
414 }
415 $tempFile2 = fopen( $tempName2, "w" );
416 if ( !$tempFile2 ) {
417 //wfProfileOut( $fname );
418 return false;
419 }
420 fwrite( $tempFile1, $otext );
421 fwrite( $tempFile2, $ntext );
422 fclose( $tempFile1 );
423 fclose( $tempFile2 );
424 $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 );
425 //wfProfileIn( "$fname-shellexec" );
426 $difftext = wfShellExec( $cmd );
427 //wfProfileOut( "$fname-shellexec" );
428 unlink( $tempName1 );
429 unlink( $tempName2 );
430 return $difftext;
431 }
432
433 # Native PHP diff
434 $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) );
435 $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) );
436 $diffs = new Diff( $ota, $nta );
437 $formatter = new TableDiffFormatter();
438 return $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) );
439 }
440
441
446 return preg_replace_callback( '/<!--LINE (\d+)-->/',
447 array( &$this, 'localiseLineNumbersCb' ), $text );
448 }
449
450 function localiseLineNumbersCb( $matches ) {
451 global $wgLang;
452 return wfMsgExt( 'lineno', array('parseinline'), $wgLang->formatNum( $matches[1] ) );
453 }
454
455
459 function getMultiNotice() {
460 if ( !is_object($this->mOldRev) || !is_object($this->mNewRev) )
461 return '';
462
463 if( !$this->mOldPage->equals( $this->mNewPage ) ) {
464 // Comparing two different pages? Count would be meaningless.
465 return '';
466 }
467
468 $oldid = $this->mOldRev->getId();
469 $newid = $this->mNewRev->getId();
470 if ( $oldid > $newid ) {
471 $tmp = $oldid; $oldid = $newid; $newid = $tmp;
472 }
473
474 $n = $this->mTitle->countRevisionsBetween( $oldid, $newid );
475 if ( !$n )
476 return '';
477
478 return wfMsgExt( 'diff-multi', array( 'parseinline' ), $n );
479 }
480
481
485 function addHeader( $diff, $otitle, $ntitle, $multi = '' ) {
486 global $wgOut;
487
488 if ( $this->mOldRev && $this->mOldRev->isDeleted(Revision::DELETED_TEXT) ) {
489 $otitle = '<span class="history-deleted">'.$otitle.'</span>';
490 }
491 if ( $this->mNewRev && $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
492 $ntitle = '<span class="history-deleted">'.$ntitle.'</span>';
493 }
494 $header = "
495 <table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'>
496 <tr>
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>
499 </tr>
500 ";
501
502 if ( $multi != '' )
503 $header .= "<tr><td colspan='4' align='center' class='diff-multi'>{$multi}</td></tr>";
504
505 return $header . $diff . "</table>";
506 }
507
511 function setText( $oldText, $newText ) {
512 $this->mOldtext = $oldText;
513 $this->mNewtext = $newText;
514 $this->mTextLoaded = 2;
515 }
516
527 function loadRevisionData() {
528 global $wgLang;
529 if ( $this->mRevisionsLoaded ) {
530 return true;
531 } else {
532 // Whether it succeeds or fails, we don't want to try again
533 $this->mRevisionsLoaded = true;
534 }
535
536 // Load the new revision object
537 if( $this->mNewid ) {
538 $this->mNewRev = Revision::newFromId( $this->mNewid );
539 } else {
540 $this->mNewRev = Revision::newFromTitle( $this->mTitle );
541 }
542
543 if( is_null( $this->mNewRev ) ) {
544 return false;
545 }
546
547 // Set assorted variables
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' );
554
555 $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a> ($timestamp)"
556 . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)";
557
558 } else {
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 ) );
562
563 $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>"
564 . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)";
565 }
566
567 // Load the old revision object
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();
573 if( $rev ) {
574 $this->mOldid = $rev->getId();
575 $this->mOldRev = $rev;
576 } else {
577 // No previous revision; mark to show as first-version only.
578 $this->mOldid = false;
579 $this->mOldRev = false;
580 }
581 }/* elseif ( $this->mOldid === false ) leave mOldRev false; */
582
583 if( is_null( $this->mOldRev ) ) {
584 return false;
585 }
586
587 if ( $this->mOldRev ) {
588 $this->mOldPage = $this->mOldRev->getTitle();
589
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>)";
595 //now that we considered old rev, we can make undo link (bug 8133, multi-edit undo)
596 $newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undoafter=' . $this->mOldid . '&undo=' . $this->mNewid);
597 $this->mNewtitle .= " (<a href='$newUndo'>" . htmlspecialchars( wfMsg( 'editundo' ) ) . "</a>)";
598 }
599
600 return true;
601 }
602
606 function loadText() {
607 if ( $this->mTextLoaded == 2 ) {
608 return true;
609 } else {
610 // Whether it succeeds or fails, we don't want to try again
611 $this->mTextLoaded = 2;
612 }
613
614 if ( !$this->loadRevisionData() ) {
615 return false;
616 }
617 if ( $this->mOldRev ) {
618 // FIXME: permission tests
619 $this->mOldtext = $this->mOldRev->revText();
620 if ( $this->mOldtext === false ) {
621 return false;
622 }
623 }
624 if ( $this->mNewRev ) {
625 $this->mNewtext = $this->mNewRev->revText();
626 if ( $this->mNewtext === false ) {
627 return false;
628 }
629 }
630 return true;
631 }
632
636 function loadNewText() {
637 if ( $this->mTextLoaded >= 1 ) {
638 return true;
639 } else {
640 $this->mTextLoaded = 1;
641 }
642 if ( !$this->loadRevisionData() ) {
643 return false;
644 }
645 $this->mNewtext = $this->mNewRev->getText();
646 return true;
647 }
648
649
650}
651
652// A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
653//
654// Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
655// You may copy this code freely under the conditions of the GPL.
656//
657
658define('USE_ASSERTS', function_exists('assert'));
659
665class _DiffOp {
666 var $type;
667 var $orig;
669
670 function reverse() {
671 trigger_error('pure virtual', E_USER_ERROR);
672 }
673
674 function norig() {
675 return $this->orig ? sizeof($this->orig) : 0;
676 }
677
678 function nclosing() {
679 return $this->closing ? sizeof($this->closing) : 0;
680 }
681}
682
688class _DiffOp_Copy extends _DiffOp {
689 var $type = 'copy';
690
691 function _DiffOp_Copy ($orig, $closing = false) {
692 if (!is_array($closing))
693 $closing = $orig;
694 $this->orig = $orig;
695 $this->closing = $closing;
696 }
697
698 function reverse() {
699 return new _DiffOp_Copy($this->closing, $this->orig);
700 }
701}
702
708class _DiffOp_Delete extends _DiffOp {
709 var $type = 'delete';
710
711 function _DiffOp_Delete ($lines) {
712 $this->orig = $lines;
713 $this->closing = false;
714 }
715
716 function reverse() {
717 return new _DiffOp_Add($this->orig);
718 }
719}
720
726class _DiffOp_Add extends _DiffOp {
727 var $type = 'add';
728
729 function _DiffOp_Add ($lines) {
730 $this->closing = $lines;
731 $this->orig = false;
732 }
733
734 function reverse() {
735 return new _DiffOp_Delete($this->closing);
736 }
737}
738
744class _DiffOp_Change extends _DiffOp {
745 var $type = 'change';
746
748 $this->orig = $orig;
749 $this->closing = $closing;
750 }
751
752 function reverse() {
753 return new _DiffOp_Change($this->closing, $this->orig);
754 }
755}
756
757
782{
783 const MAX_XREF_LENGTH = 10000;
784
785 function diff ($from_lines, $to_lines) {
786 $fname = '_DiffEngine::diff';
787 //wfProfileIn( $fname );
788
789 $n_from = sizeof($from_lines);
790 $n_to = sizeof($to_lines);
791
792 $this->xchanged = $this->ychanged = array();
793 $this->xv = $this->yv = array();
794 $this->xind = $this->yind = array();
795 unset($this->seq);
796 unset($this->in_seq);
797 unset($this->lcs);
798
799 // Skip leading common lines.
800 for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
801 if ($from_lines[$skip] !== $to_lines[$skip])
802 break;
803 $this->xchanged[$skip] = $this->ychanged[$skip] = false;
804 }
805 // Skip trailing common lines.
806 $xi = $n_from; $yi = $n_to;
807 for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
808 if ($from_lines[$xi] !== $to_lines[$yi])
809 break;
810 $this->xchanged[$xi] = $this->ychanged[$yi] = false;
811 }
812
813 // Ignore lines which do not exist in both files.
814 for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
815 $xhash[$this->_line_hash($from_lines[$xi])] = 1;
816 }
817
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)])) )
821 continue;
822 $yhash[$this->_line_hash($line)] = 1;
823 $this->yv[] = $line;
824 $this->yind[] = $yi;
825 }
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)])) )
829 continue;
830 $this->xv[] = $line;
831 $this->xind[] = $xi;
832 }
833
834 // Find the LCS.
835 $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
836
837 // Merge edits when possible
838 $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
839 $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
840
841 // Compute the edit operations.
842 $edits = array();
843 $xi = $yi = 0;
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]);
847
848 // Skip matching "snake".
849 $copy = array();
850 while ( $xi < $n_from && $yi < $n_to
851 && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
852 $copy[] = $from_lines[$xi++];
853 ++$yi;
854 }
855 if ($copy)
856 $edits[] = new _DiffOp_Copy($copy);
857
858 // Find deletes & adds.
859 $delete = array();
860 while ($xi < $n_from && $this->xchanged[$xi])
861 $delete[] = $from_lines[$xi++];
862
863 $add = array();
864 while ($yi < $n_to && $this->ychanged[$yi])
865 $add[] = $to_lines[$yi++];
866
867 if ($delete && $add)
868 $edits[] = new _DiffOp_Change($delete, $add);
869 elseif ($delete)
870 $edits[] = new _DiffOp_Delete($delete);
871 elseif ($add)
872 $edits[] = new _DiffOp_Add($add);
873 }
874 //wfProfileOut( $fname );
875 return $edits;
876 }
877
881 function _line_hash( $line ) {
882 if ( strlen( $line ) > self::MAX_XREF_LENGTH ) {
883 return md5( $line );
884 } else {
885 return $line;
886 }
887 }
888
889
890 /* Divide the Largest Common Subsequence (LCS) of the sequences
891 * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
892 * sized segments.
893 *
894 * Returns (LCS, PTS). LCS is the length of the LCS. PTS is an
895 * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
896 * sub sequences. The first sub-sequence is contained in [X0, X1),
897 * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on. Note
898 * that (X0, Y0) == (XOFF, YOFF) and
899 * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
900 *
901 * This function assumes that the first lines of the specified portions
902 * of the two files do not match, and likewise that the last lines do not
903 * match. The caller must trim matching lines from the beginning and end
904 * of the portions it is going to specify.
905 */
906 function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks) {
907 $fname = '_DiffEngine::_diag';
908 //wfProfileIn( $fname );
909 $flip = false;
910
911 if ($xlim - $xoff > $ylim - $yoff) {
912 // Things seems faster (I'm not sure I understand why)
913 // when the shortest sequence in X.
914 $flip = true;
915 list ($xoff, $xlim, $yoff, $ylim)
916 = array( $yoff, $ylim, $xoff, $xlim);
917 }
918
919 if ($flip)
920 for ($i = $ylim - 1; $i >= $yoff; $i--)
921 $ymatches[$this->xv[$i]][] = $i;
922 else
923 for ($i = $ylim - 1; $i >= $yoff; $i--)
924 $ymatches[$this->yv[$i]][] = $i;
925
926 $this->lcs = 0;
927 $this->seq[0]= $yoff - 1;
928 $this->in_seq = array();
929 $ymids[0] = array();
930
931 $numer = $xlim - $xoff + $nchunks - 1;
932 $x = $xoff;
933 for ($chunk = 0; $chunk < $nchunks; $chunk++) {
934 //wfProfileIn( "$fname-chunk" );
935 if ($chunk > 0)
936 for ($i = 0; $i <= $this->lcs; $i++)
937 $ymids[$i][$chunk-1] = $this->seq[$i];
938
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]))
943 continue;
944 $matches = $ymatches[$line];
945 reset($matches);
946 while (list ($junk, $y) = each($matches))
947 if (empty($this->in_seq[$y])) {
948 $k = $this->_lcs_pos($y);
949 USE_ASSERTS && assert($k > 0);
950 $ymids[$k] = $ymids[$k-1];
951 break;
952 }
953 while (list ( /* $junk */, $y) = each($matches)) {
954 if ($y > $this->seq[$k-1]) {
955 USE_ASSERTS && assert($y < $this->seq[$k]);
956 // Optimization: this is a common case:
957 // next match is just replacing previous match.
958 $this->in_seq[$this->seq[$k]] = false;
959 $this->seq[$k] = $y;
960 $this->in_seq[$y] = 1;
961 } else if (empty($this->in_seq[$y])) {
962 $k = $this->_lcs_pos($y);
963 USE_ASSERTS && assert($k > 0);
964 $ymids[$k] = $ymids[$k-1];
965 }
966 }
967 }
968 //wfProfileOut( "$fname-chunk" );
969 }
970
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);
975 $y1 = $ymid[$n] + 1;
976 $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
977 }
978 $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
979
980 //wfProfileOut( $fname );
981 return array($this->lcs, $seps);
982 }
983
984 function _lcs_pos ($ypos) {
985 $fname = '_DiffEngine::_lcs_pos';
986 //wfProfileIn( $fname );
987
988 $end = $this->lcs;
989 if ($end == 0 || $ypos > $this->seq[$end]) {
990 $this->seq[++$this->lcs] = $ypos;
991 $this->in_seq[$ypos] = 1;
992 //wfProfileOut( $fname );
993 return $this->lcs;
994 }
995
996 $beg = 1;
997 while ($beg < $end) {
998 $mid = (int)(($beg + $end) / 2);
999 if ( $ypos > $this->seq[$mid] )
1000 $beg = $mid + 1;
1001 else
1002 $end = $mid;
1003 }
1004
1005 USE_ASSERTS && assert($ypos != $this->seq[$end]);
1006
1007 $this->in_seq[$this->seq[$end]] = false;
1008 $this->seq[$end] = $ypos;
1009 $this->in_seq[$ypos] = 1;
1010 //wfProfileOut( $fname );
1011 return $end;
1012 }
1013
1014 /* Find LCS of two sequences.
1015 *
1016 * The results are recorded in the vectors $this->{x,y}changed[], by
1017 * storing a 1 in the element for each line that is an insertion
1018 * or deletion (ie. is not in the LCS).
1019 *
1020 * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
1021 *
1022 * Note that XLIM, YLIM are exclusive bounds.
1023 * All line numbers are origin-0 and discarded lines are not counted.
1024 */
1025 function _compareseq ($xoff, $xlim, $yoff, $ylim) {
1026 $fname = '_DiffEngine::_compareseq';
1027 //wfProfileIn( $fname );
1028
1029 // Slide down the bottom initial diagonal.
1030 while ($xoff < $xlim && $yoff < $ylim
1031 && $this->xv[$xoff] == $this->yv[$yoff]) {
1032 ++$xoff;
1033 ++$yoff;
1034 }
1035
1036 // Slide up the top initial diagonal.
1037 while ($xlim > $xoff && $ylim > $yoff
1038 && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
1039 --$xlim;
1040 --$ylim;
1041 }
1042
1043 if ($xoff == $xlim || $yoff == $ylim)
1044 $lcs = 0;
1045 else {
1046 // This is ad hoc but seems to work well.
1047 //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
1048 //$nchunks = max(2,min(8,(int)$nchunks));
1049 $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
1050 list ($lcs, $seps)
1051 = $this->_diag($xoff,$xlim,$yoff, $ylim,$nchunks);
1052 }
1053
1054 if ($lcs == 0) {
1055 // X and Y sequences have no common subsequence:
1056 // mark all changed.
1057 while ($yoff < $ylim)
1058 $this->ychanged[$this->yind[$yoff++]] = 1;
1059 while ($xoff < $xlim)
1060 $this->xchanged[$this->xind[$xoff++]] = 1;
1061 } else {
1062 // Use the partitions to split this problem into subproblems.
1063 reset($seps);
1064 $pt1 = $seps[0];
1065 while ($pt2 = next($seps)) {
1066 $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
1067 $pt1 = $pt2;
1068 }
1069 }
1070 //wfProfileOut( $fname );
1071 }
1072
1073 /* Adjust inserts/deletes of identical lines to join changes
1074 * as much as possible.
1075 *
1076 * We do something when a run of changed lines include a
1077 * line at one end and has an excluded, identical line at the other.
1078 * We are free to choose which identical line is included.
1079 * `compareseq' usually chooses the one at the beginning,
1080 * but usually it is cleaner to consider the following identical line
1081 * to be the "change".
1082 *
1083 * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
1084 */
1085 function _shift_boundaries ($lines, &$changed, $other_changed) {
1086 $fname = '_DiffEngine::_shift_boundaries';
1087 //wfProfileIn( $fname );
1088 $i = 0;
1089 $j = 0;
1090
1091 USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
1092 $len = sizeof($lines);
1093 $other_len = sizeof($other_changed);
1094
1095 while (1) {
1096 /*
1097 * Scan forwards to find beginning of another run of changes.
1098 * Also keep track of the corresponding point in the other file.
1099 *
1100 * Throughout this code, $i and $j are adjusted together so that
1101 * the first $i elements of $changed and the first $j elements
1102 * of $other_changed both contain the same number of zeros
1103 * (unchanged lines).
1104 * Furthermore, $j is always kept so that $j == $other_len or
1105 * $other_changed[$j] == false.
1106 */
1107 while ($j < $other_len && $other_changed[$j])
1108 $j++;
1109
1110 while ($i < $len && ! $changed[$i]) {
1111 USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
1112 $i++; $j++;
1113 while ($j < $other_len && $other_changed[$j])
1114 $j++;
1115 }
1116
1117 if ($i == $len)
1118 break;
1119
1120 $start = $i;
1121
1122 // Find the end of this run of changes.
1123 while (++$i < $len && $changed[$i])
1124 continue;
1125
1126 do {
1127 /*
1128 * Record the length of this run of changes, so that
1129 * we can later determine whether the run has grown.
1130 */
1131 $runlength = $i - $start;
1132
1133 /*
1134 * Move the changed region back, so long as the
1135 * previous unchanged line matches the last changed one.
1136 * This merges with previous changed regions.
1137 */
1138 while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
1139 $changed[--$start] = 1;
1140 $changed[--$i] = false;
1141 while ($start > 0 && $changed[$start - 1])
1142 $start--;
1143 USE_ASSERTS && assert('$j > 0');
1144 while ($other_changed[--$j])
1145 continue;
1146 USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
1147 }
1148
1149 /*
1150 * Set CORRESPONDING to the end of the changed run, at the last
1151 * point where it corresponds to a changed run in the other file.
1152 * CORRESPONDING == LEN means no such point has been found.
1153 */
1154 $corresponding = $j < $other_len ? $i : $len;
1155
1156 /*
1157 * Move the changed region forward, so long as the
1158 * first changed line matches the following unchanged one.
1159 * This merges with following changed regions.
1160 * Do this second, so that if there are no merges,
1161 * the changed region is moved forward as far as possible.
1162 */
1163 while ($i < $len && $lines[$start] == $lines[$i]) {
1164 $changed[$start++] = false;
1165 $changed[$i++] = 1;
1166 while ($i < $len && $changed[$i])
1167 $i++;
1168
1169 USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
1170 $j++;
1171 if ($j < $other_len && $other_changed[$j]) {
1172 $corresponding = $i;
1173 while ($j < $other_len && $other_changed[$j])
1174 $j++;
1175 }
1176 }
1177 } while ($runlength != $i - $start);
1178
1179 /*
1180 * If possible, move the fully-merged run of changes
1181 * back to a corresponding run in the other file.
1182 */
1183 while ($corresponding < $i) {
1184 $changed[--$start] = 1;
1185 $changed[--$i] = 0;
1186 USE_ASSERTS && assert('$j > 0');
1187 while ($other_changed[--$j])
1188 continue;
1189 USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
1190 }
1191 }
1192 //wfProfileOut( $fname );
1193 }
1194}
1195
1202class Diff
1203{
1205
1214 function Diff($from_lines, $to_lines) {
1215 $eng = new _DiffEngine;
1216 $this->edits = $eng->diff($from_lines, $to_lines);
1217 //$this->_check($from_lines, $to_lines);
1218 }
1219
1230 function reverse () {
1231 $rev = $this;
1232 $rev->edits = array();
1233 foreach ($this->edits as $edit) {
1234 $rev->edits[] = $edit->reverse();
1235 }
1236 return $rev;
1237 }
1238
1244 function isEmpty () {
1245 foreach ($this->edits as $edit) {
1246 if ($edit->type != 'copy')
1247 return false;
1248 }
1249 return true;
1250 }
1251
1259 function lcs () {
1260 $lcs = 0;
1261 foreach ($this->edits as $edit) {
1262 if ($edit->type == 'copy')
1263 $lcs += sizeof($edit->orig);
1264 }
1265 return $lcs;
1266 }
1267
1276 function orig() {
1277 $lines = array();
1278
1279 foreach ($this->edits as $edit) {
1280 if ($edit->orig)
1281 array_splice($lines, sizeof($lines), 0, $edit->orig);
1282 }
1283 return $lines;
1284 }
1285
1294 function closing() {
1295 $lines = array();
1296
1297 foreach ($this->edits as $edit) {
1298 if ($edit->closing)
1299 array_splice($lines, sizeof($lines), 0, $edit->closing);
1300 }
1301 return $lines;
1302 }
1303
1309 function _check ($from_lines, $to_lines) {
1310 $fname = 'Diff::_check';
1311 //wfProfileIn( $fname );
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);
1316
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);
1322
1323
1324 $prevtype = 'none';
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;
1329 }
1330
1331 $lcs = $this->lcs();
1332 trigger_error('Diff okay: LCS = '.$lcs, E_USER_NOTICE);
1333 //wfProfileOut( $fname );
1334 }
1335}
1336
1342class MappedDiff extends Diff
1343{
1367 function MappedDiff($from_lines, $to_lines,
1368 $mapped_from_lines, $mapped_to_lines) {
1369 $fname = 'MappedDiff::MappedDiff';
1370 //wfProfileIn( $fname );
1371
1372 assert(sizeof($from_lines) == sizeof($mapped_from_lines));
1373 assert(sizeof($to_lines) == sizeof($mapped_to_lines));
1374
1375 $this->Diff($mapped_from_lines, $mapped_to_lines);
1376
1377 $xi = $yi = 0;
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);
1383 }
1384
1385 $closing = &$this->edits[$i]->closing;
1386 if (is_array($closing)) {
1387 $closing = array_slice($to_lines, $yi, sizeof($closing));
1388 $yi += sizeof($closing);
1389 }
1390 }
1391 //wfProfileOut( $fname );
1392 }
1393}
1394
1406{
1414
1422
1429 function format($diff) {
1430 $fname = 'DiffFormatter::format';
1431 //wfProfileIn( $fname );
1432
1433 $xi = $yi = 1;
1434 $block = false;
1435 $context = array();
1436
1439
1440 $this->_start_diff();
1441
1442 foreach ($diff->edits as $edit) {
1443 if ($edit->type == 'copy') {
1444 if (is_array($block)) {
1445 if (sizeof($edit->orig) <= $nlead + $ntrail) {
1446 $block[] = $edit;
1447 }
1448 else{
1449 if ($ntrail) {
1450 $context = array_slice($edit->orig, 0, $ntrail);
1451 $block[] = new _DiffOp_Copy($context);
1452 }
1453 $this->_block($x0, $ntrail + $xi - $x0,
1454 $y0, $ntrail + $yi - $y0,
1455 $block);
1456 $block = false;
1457 }
1458 }
1459 $context = $edit->orig;
1460 }
1461 else {
1462 if (! is_array($block)) {
1463 $context = array_slice($context, sizeof($context) - $nlead);
1464 $x0 = $xi - sizeof($context);
1465 $y0 = $yi - sizeof($context);
1466 $block = array();
1467 if ($context)
1468 $block[] = new _DiffOp_Copy($context);
1469 }
1470 $block[] = $edit;
1471 }
1472
1473 if ($edit->orig)
1474 $xi += sizeof($edit->orig);
1475 if ($edit->closing)
1476 $yi += sizeof($edit->closing);
1477 }
1478
1479 if (is_array($block))
1480 $this->_block($x0, $xi - $x0,
1481 $y0, $yi - $y0,
1482 $block);
1483
1484 $end = $this->_end_diff();
1485 //wfProfileOut( $fname );
1486 return $end;
1487 }
1488
1489 function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
1490 $fname = 'DiffFormatter::_block';
1491 //wfProfileIn( $fname );
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);
1502 else
1503 trigger_error('Unknown edit type', E_USER_ERROR);
1504 }
1505 $this->_end_block();
1506 //wfProfileOut( $fname );
1507 }
1508
1509 function _start_diff() {
1510 ob_start();
1511 }
1512
1513 function _end_diff() {
1514 $val = ob_get_contents();
1515 ob_end_clean();
1516 return $val;
1517 }
1518
1519 function _block_header($xbeg, $xlen, $ybeg, $ylen) {
1520 if ($xlen > 1)
1521 $xbeg .= "," . ($xbeg + $xlen - 1);
1522 if ($ylen > 1)
1523 $ybeg .= "," . ($ybeg + $ylen - 1);
1524
1525 return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
1526 }
1527
1529 echo $header;
1530 }
1531
1532 function _end_block() {
1533 }
1534
1535 function _lines($lines, $prefix = ' ') {
1536 foreach ($lines as $line)
1537 echo "$prefix $line\n";
1538 }
1539
1540 function _context($lines) {
1541 $this->_lines($lines);
1542 }
1543
1544 function _added($lines) {
1545 $this->_lines($lines, '>');
1546 }
1547 function _deleted($lines) {
1548 $this->_lines($lines, '<');
1549 }
1550
1551 function _changed($orig, $closing) {
1552 $this->_deleted($orig);
1553 echo "---\n";
1554 $this->_added($closing);
1555 }
1556}
1557
1558
1564define('NBSP', '&#160;'); // iso-8859-x non-breaking space.
1565
1573 $this->_lines = array();
1574 $this->_line = '';
1575 $this->_group = '';
1576 $this->_tag = '';
1577 }
1578
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]';
1587 else
1588 $this->_line .= htmlspecialchars ( $this->_group );
1589 }
1590 $this->_group = '';
1591 $this->_tag = $new_tag;
1592 }
1593
1594 function _flushLine ($new_tag) {
1595 $this->_flushGroup($new_tag);
1596 if ($this->_line != '')
1597 array_push ( $this->_lines, $this->_line );
1598 else
1599 # make empty lines visible by inserting an NBSP
1600 array_push ( $this->_lines, NBSP );
1601 $this->_line = '';
1602 }
1603
1604 function addWords ($words, $tag = '') {
1605 if ($tag != $this->_tag)
1606 $this->_flushGroup($tag);
1607
1608 foreach ($words as $word) {
1609 // new-line should only come as first char of word.
1610 if ($word == '')
1611 continue;
1612 if ($word[0] == "\n") {
1613 $this->_flushLine($tag);
1614 $word = substr($word, 1);
1615 }
1616 assert(!strstr($word, "\n"));
1617 $this->_group .= $word;
1618 }
1619 }
1620
1621 function getLines() {
1622 $this->_flushLine('~done');
1623 return $this->_lines;
1624 }
1625}
1626
1633{
1634 const MAX_LINE_LENGTH = 10000;
1635
1636 function WordLevelDiff ($orig_lines, $closing_lines) {
1637 $fname = 'WordLevelDiff::WordLevelDiff';
1638 //wfProfileIn( $fname );
1639
1640 list ($orig_words, $orig_stripped) = $this->_split($orig_lines);
1641 list ($closing_words, $closing_stripped) = $this->_split($closing_lines);
1642
1643 $this->MappedDiff($orig_words, $closing_words,
1644 $orig_stripped, $closing_stripped);
1645 //wfProfileOut( $fname );
1646 }
1647
1648 function _split($lines) {
1649 $fname = 'WordLevelDiff::_split';
1650 //wfProfileIn( $fname );
1651
1652 $words = array();
1653 $stripped = array();
1654 $first = true;
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
1658 if ( $first ) {
1659 $first = false;
1660 } else {
1661 $words[] = "\n";
1662 $stripped[] = "\n";
1663 }
1664 if ( strlen( $line ) > self::MAX_LINE_LENGTH ) {
1665 $words[] = $line;
1666 $stripped[] = $line;
1667 } else {
1668 $m = array();
1669 if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs',
1670 $line, $m))
1671 {
1672 $words = array_merge( $words, $m[0] );
1673 $stripped = array_merge( $stripped, $m[1] );
1674 }
1675 }
1676 }
1677 //wfProfileOut( $fname );
1678 return array($words, $stripped);
1679 }
1680
1681 function orig () {
1682 $fname = 'WordLevelDiff::orig';
1683 //wfProfileIn( $fname );
1684 $orig = new _HWLDF_WordAccumulator;
1685
1686 foreach ($this->edits as $edit) {
1687 if ($edit->type == 'copy')
1688 $orig->addWords($edit->orig);
1689 elseif ($edit->orig)
1690 $orig->addWords($edit->orig, 'del');
1691 }
1692 $lines = $orig->getLines();
1693 //wfProfileOut( $fname );
1694 return $lines;
1695 }
1696
1697 function closing () {
1698 $fname = 'WordLevelDiff::closing';
1699 //wfProfileIn( $fname );
1700 $closing = new _HWLDF_WordAccumulator;
1701
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');
1707 }
1708 $lines = $closing->getLines();
1709 //wfProfileOut( $fname );
1710 return $lines;
1711 }
1712}
1713
1721{
1723 $this->leading_context_lines = 2;
1724 $this->trailing_context_lines = 2;
1725 }
1726
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";
1730 return $r;
1731 }
1732
1733 function _start_block( $header ) {
1734 echo $header;
1735 }
1736
1737 function _end_block() {
1738 }
1739
1740 function _lines( $lines, $prefix=' ', $color='white' ) {
1741 }
1742
1743 # HTML-escape parameter before calling this
1744 function addedLine( $line ) {
1745 return "<td>+</td><td class='diff-addedline'>{$line}</td>";
1746 }
1747
1748 # HTML-escape parameter before calling this
1749 function deletedLine( $line ) {
1750 return "<td>-</td><td class='diff-deletedline'>{$line}</td>";
1751 }
1752
1753 # HTML-escape parameter before calling this
1754 function contextLine( $line ) {
1755 return "<td> </td><td class='diff-context'>{$line}</td>";
1756 }
1757
1758 function emptyLine() {
1759 return '<td colspan="2">&nbsp;</td>';
1760 }
1761
1762 function _added( $lines ) {
1763 foreach ($lines as $line) {
1764 echo '<tr>' . $this->emptyLine() .
1765 $this->addedLine( htmlspecialchars ( $line ) ) . "</tr>\n";
1766 }
1767 }
1768
1769 function _deleted($lines) {
1770 foreach ($lines as $line) {
1771 echo '<tr>' . $this->deletedLine( htmlspecialchars ( $line ) ) .
1772 $this->emptyLine() . "</tr>\n";
1773 }
1774 }
1775
1776 function _context( $lines ) {
1777 foreach ($lines as $line) {
1778 echo '<tr>' .
1779 $this->contextLine( htmlspecialchars ( $line ) ) .
1780 $this->contextLine( htmlspecialchars ( $line ) ) . "</tr>\n";
1781 }
1782 }
1783
1784 function _changed( $orig, $closing ) {
1785 $fname = 'TableDiffFormatter::_changed';
1786 //wfProfileIn( $fname );
1787
1788 $diff = new WordLevelDiff( $orig, $closing );
1789 $del = $diff->orig();
1790 $add = $diff->closing();
1791
1792 # Notice that WordLevelDiff returns HTML-escaped output.
1793 # Hence, we will be calling addedLine/deletedLine without HTML-escaping.
1794
1795 while ( $line = array_shift( $del ) ) {
1796 $aline = array_shift( $add );
1797 echo '<tr>' . $this->deletedLine( $line ) .
1798 $this->addedLine( $aline ) . "</tr>\n";
1799 }
1800 foreach ($add as $line) { # If any leftovers
1801 echo '<tr>' . $this->emptyLine() .
1802 $this->addedLine( $line ) . "</tr>\n";
1803 }
1804 //wfProfileOut( $fname );
1805 }
1806}
1807
1808?>
wfMsg($x)
Definition: RandomTest.php:60
$n
Definition: RandomTest.php:80
foreach($mandatory_scripts as $file) $timestamp
Definition: buildRTE.php:81
_block_header($xbeg, $xlen, $ybeg, $ylen)
_changed($orig, $closing)
_lines($lines, $prefix=' ')
$leading_context_lines
Number of leading context "lines" to preserve.
format($diff)
Format a diff.
_block($xbeg, $xlen, $ybeg, $ylen, &$edits)
$trailing_context_lines
Number of trailing context "lines" to preserve.
reverse()
Compute reversed Diff.
orig()
Get the original set of lines.
isEmpty()
Check for empty diff.
lcs()
Compute the length of the Longest Common Subsequence (LCS).
Diff($from_lines, $to_lines)
Constructor.
closing()
Get the closing set of lines.
_check($from_lines, $to_lines)
Check a Diff for validity.
showFirstRevision()
Show the first revision of an article.
loadNewText()
Load the text of the new revision, not the old one.
showDiffPage( $diffOnly=false)
loadText()
Load the text of the revisions, as well as revision data.
getDiffBody()
Get the diff table body, without header Results are cached Returns false on error.
loadRevisionData()
Load revision metadata for the specified articles.
generateDiffBody( $otext, $ntext)
Generate a diff, no caching $otext and $ntext must be already segmented.
localiseLineNumbers( $text)
Replace line numbers with the text in the user's language.
localiseLineNumbersCb( $matches)
setText( $oldText, $newText)
Use specified text instead of loading from the database.
getMultiNotice()
If there are revisions between the ones being compared, return a note saying so.
DifferenceEngine( $titleObj=null, $old=0, $new=0, $rcid=0)
#-
showDiff( $otitle, $ntitle)
Get the diff text, send it to $wgOut Returns false if the diff could not be generated,...
renderNewRevision()
Show the new revision of the page.
getDiff( $otitle, $ntitle)
Get diff table, including header Note that the interface has changed, it's no longer static.
addHeader( $diff, $otitle, $ntitle, $multi='')
Add the header to a diff body.
MappedDiff($from_lines, $to_lines, $mapped_from_lines, $mapped_to_lines)
Constructor.
_block_header( $xbeg, $xlen, $ybeg, $ylen)
_changed( $orig, $closing)
_lines( $lines, $prefix=' ', $color='white')
WordLevelDiff($orig_lines, $closing_lines)
closing()
Get the closing set of lines.
orig()
Get the original set of lines.
const NBSP
Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3.
const USE_ASSERTS
_diag($xoff, $xlim, $yoff, $ylim, $nchunks)
_line_hash( $line)
Returns the whole line if it's small enough, or the MD5 hash otherwise.
diff($from_lines, $to_lines)
_shift_boundaries($lines, &$changed, $other_changed)
_compareseq($xoff, $xlim, $yoff, $ylim)
_DiffOp_Change($orig, $closing)
_DiffOp_Copy($orig, $closing=false)
$y
Definition: example_007.php:83
$x
Definition: example_009.php:98
$header
$text
$r
Definition: example_031.php:79
exit
Definition: login.php:54
$cmd
Definition: sahs_server.php:35