ILIAS  release_5-3 Revision v5.3.23-19-g915713cf615
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
18{
22 public $mOldid;
23 public $mNewid;
24 public $mTitle;
25 public $mOldtitle;
26 public $mNewtitle;
28 public $mOldtext;
29 public $mNewtext;
30 public $mOldPage;
31 public $mNewPage;
33 public $mOldRev;
34 public $mNewRev;
35 public $mRevisionsLoaded = false; // Have the revisions been loaded
36 public $mTextLoaded = 0; // How many text blobs have been loaded, 0, 1 or 2?
46 public function __construct($titleObj = null, $old = 0, $new = 0, $rcid = 0)
47 {
48 $this->mTitle = $titleObj;
49 wfDebug("DifferenceEngine old '$old' new '$new' rcid '$rcid'\n");
50
51 if ('prev' === $new) {
52 # Show diff between revision $old and the previous one.
53 # Get previous one from DB.
54 #
55 $this->mNewid = intval($old);
56
57 $this->mOldid = $this->mTitle->getPreviousRevisionID($this->mNewid);
58 } elseif ('next' === $new) {
59 # Show diff between revision $old and the previous one.
60 # Get previous one from DB.
61 #
62 $this->mOldid = intval($old);
63 $this->mNewid = $this->mTitle->getNextRevisionID($this->mOldid);
64 if (false === $this->mNewid) {
65 # if no result, NewId points to the newest old revision. The only newer
66 # revision is cur, which is "0".
67 $this->mNewid = 0;
68 }
69 } else {
70 $this->mOldid = intval($old);
71 $this->mNewid = intval($new);
72 }
73 $this->mRcidMarkPatrolled = intval($rcid); # force it to be an integer
74 }
75
76 public function showDiffPage($diffOnly = false)
77 {
78 global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol;
79 $fname = 'DifferenceEngine::showDiffPage';
80 //wfProfileIn( $fname );
81
82 # If external diffs are enabled both globally and for the user,
83 # we'll use the application/x-external-editor interface to call
84 # an external diff tool like kompare, kdiff3, etc.
85 if ($wgUseExternalEditor && $wgUser->getOption('externaldiff')) {
86 global $wgInputEncoding,$wgServer,$wgScript,$wgLang;
87 $wgOut->disable();
88 header("Content-type: application/x-external-editor; charset=" . $wgInputEncoding);
89 $url1=$this->mTitle->getFullURL("action=raw&oldid=" . $this->mOldid);
90 $url2=$this->mTitle->getFullURL("action=raw&oldid=" . $this->mNewid);
91 $special=$wgLang->getNsText(NS_SPECIAL);
92 $control=<<<CONTROL
93[Process]
94Type=Diff text
95Engine=MediaWiki
96Script={$wgServer}{$wgScript}
97Special namespace={$special}
98
99[File]
100Extension=wiki
101URL=$url1
102
103[File 2]
104Extension=wiki
105URL=$url2
106CONTROL;
107 echo($control);
108 return;
109 }
110
111 $wgOut->setArticleFlag(false);
112 if (!$this->loadRevisionData()) {
113 $t = $this->mTitle->getPrefixedText() . " (Diff: {$this->mOldid}, {$this->mNewid})";
114 $mtext = wfMsg('missingarticle', "<nowiki>$t</nowiki>");
115 $wgOut->setPagetitle(wfMsg('errorpagetitle'));
116 $wgOut->addWikitext($mtext);
117 //wfProfileOut( $fname );
118 return;
119 }
120
121 wfRunHooks('DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ));
122
123 if ($this->mNewRev->isCurrent()) {
124 $wgOut->setArticleFlag(true);
125 }
126
127 # mOldid is false if the difference engine is called with a "vague" query for
128 # a diff between a version V and its previous version V' AND the version V
129 # is the first version of that article. In that case, V' does not exist.
130 if ($this->mOldid === false) {
131 $this->showFirstRevision();
132 $this->renderNewRevision(); // should we respect $diffOnly here or not?
133 //wfProfileOut( $fname );
134 return;
135 }
136
137 $wgOut->suppressQuickbar();
138
139 $oldTitle = $this->mOldPage->getPrefixedText();
140 $newTitle = $this->mNewPage->getPrefixedText();
141 if ($oldTitle == $newTitle) {
142 $wgOut->setPageTitle($newTitle);
143 } else {
144 $wgOut->setPageTitle($oldTitle . ', ' . $newTitle);
145 }
146 $wgOut->setSubtitle(wfMsg('difference'));
147 $wgOut->setRobotpolicy('noindex,nofollow');
148
149 if (!($this->mOldPage->userCanRead() && $this->mNewPage->userCanRead())) {
150 $wgOut->loginToUse();
151 $wgOut->output();
152 //wfProfileOut( $fname );
153 exit;
154 }
155
156 $sk = $wgUser->getSkin();
157
158 if ($this->mNewRev->isCurrent() && $wgUser->isAllowed('rollback')) {
159 $rollback = '&nbsp;&nbsp;&nbsp;' . $sk->generateRollback($this->mNewRev);
160 } else {
161 $rollback = '';
162 }
163 if ($wgUseRCPatrol && $this->mRcidMarkPatrolled != 0 && $wgUser->isAllowed('patrol')) {
164 $patrol = ' [' . $sk->makeKnownLinkObj($this->mTitle, wfMsg('markaspatrolleddiff'), "action=markpatrolled&rcid={$this->mRcidMarkPatrolled}") . ']';
165 } else {
166 $patrol = '';
167 }
168
169 $prevlink = $sk->makeKnownLinkObj(
170 $this->mTitle,
171 wfMsgHtml('previousdiff'),
172 'diff=prev&oldid=' . $this->mOldid,
173 '',
174 '',
175 'id="differences-prevlink"'
176 );
177 if ($this->mNewRev->isCurrent()) {
178 $nextlink = '&nbsp;';
179 } else {
180 $nextlink = $sk->makeKnownLinkObj(
181 $this->mTitle,
182 wfMsgHtml('nextdiff'),
183 'diff=next&oldid=' . $this->mNewid,
184 '',
185 '',
186 'id="differences-nextlink"'
187 );
188 }
189
190 $oldminor = '';
191 $newminor = '';
192
193 if ($this->mOldRev->mMinorEdit == 1) {
194 $oldminor = wfElement(
195 'span',
196 array( 'class' => 'minor' ),
197 wfMsg('minoreditletter')
198 ) . ' ';
199 }
200
201 if ($this->mNewRev->mMinorEdit == 1) {
202 $newminor = wfElement(
203 'span',
204 array( 'class' => 'minor' ),
205 wfMsg('minoreditletter')
206 ) . ' ';
207 }
208
209 $oldHeader = "<strong>{$this->mOldtitle}</strong><br />" .
210 $sk->revUserTools($this->mOldRev) . "<br />" .
211 $oldminor . $sk->revComment($this->mOldRev, !$diffOnly) . "<br />" .
212 $prevlink;
213 $newHeader = "<strong>{$this->mNewtitle}</strong><br />" .
214 $sk->revUserTools($this->mNewRev) . " $rollback<br />" .
215 $newminor . $sk->revComment($this->mNewRev, !$diffOnly) . "<br />" .
216 $nextlink . $patrol;
217
218 $this->showDiff($oldHeader, $newHeader);
219
220 if (!$diffOnly) {
221 $this->renderNewRevision();
222 }
223
224 //wfProfileOut( $fname );
225 }
226
230 public function renderNewRevision()
231 {
232 global $wgOut;
233 $fname = 'DifferenceEngine::renderNewRevision';
234 //wfProfileIn( $fname );
235
236 $wgOut->addHTML("<hr /><h2>{$this->mPagetitle}</h2>\n");
237 #add deleted rev tag if needed
238 if (!$this->mNewRev->userCan(Revision::DELETED_TEXT)) {
239 $wgOut->addWikiText(wfMsg('rev-deleted-text-permission'));
240 }
241
242 if (!$this->mNewRev->isCurrent()) {
243 $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection(false);
244 }
245
246 $this->loadNewText();
247 if (is_object($this->mNewRev)) {
248 $wgOut->setRevisionId($this->mNewRev->getId());
249 }
250
251 $wgOut->addWikiTextTidy($this->mNewtext);
252
253 if (!$this->mNewRev->isCurrent()) {
254 $wgOut->parserOptions()->setEditSection($oldEditSectionSetting);
255 }
256
257 //wfProfileOut( $fname );
258 }
259
264 public function showFirstRevision()
265 {
266 global $wgOut, $wgUser;
267
268 $fname = 'DifferenceEngine::showFirstRevision';
269 //wfProfileIn( $fname );
270
271 # Get article text from the DB
272 #
273 if (!$this->loadNewText()) {
274 $t = $this->mTitle->getPrefixedText() . " (Diff: {$this->mOldid}, " .
275 "{$this->mNewid})";
276 $mtext = wfMsg('missingarticle', "<nowiki>$t</nowiki>");
277 $wgOut->setPagetitle(wfMsg('errorpagetitle'));
278 $wgOut->addWikitext($mtext);
279 //wfProfileOut( $fname );
280 return;
281 }
282 if ($this->mNewRev->isCurrent()) {
283 $wgOut->setArticleFlag(true);
284 }
285
286 # Check if user is allowed to look at this page. If not, bail out.
287 #
288 if (!($this->mTitle->userCanRead())) {
289 $wgOut->loginToUse();
290 $wgOut->output();
291 //wfProfileOut( $fname );
292 exit;
293 }
294
295 # Prepare the header box
296 #
297 $sk = $wgUser->getSkin();
298
299 $nextlink = $sk->makeKnownLinkObj($this->mTitle, wfMsgHtml('nextdiff'), 'diff=next&oldid=' . $this->mNewid, '', '', 'id="differences-nextlink"');
300 $header = "<div class=\"firstrevisionheader\" style=\"text-align: center\"><strong>{$this->mOldtitle}</strong><br />" .
301 $sk->revUserTools($this->mNewRev) . "<br />" .
302 $sk->revComment($this->mNewRev) . "<br />" .
303 $nextlink . "</div>\n";
304
305 $wgOut->addHTML($header);
306
307 $wgOut->setSubtitle(wfMsg('difference'));
308 $wgOut->setRobotpolicy('noindex,nofollow');
309
310 //wfProfileOut( $fname );
311 }
312
317 public function showDiff($otitle, $ntitle)
318 {
319 global $wgOut;
320 $diff = $this->getDiff($otitle, $ntitle);
321 if ($diff === false) {
322 $wgOut->addWikitext(wfMsg('missingarticle', "<nowiki>(fixme, bug)</nowiki>"));
323 return false;
324 } else {
325 $wgOut->addHTML($diff);
326 return true;
327 }
328 }
329
335 public function getDiff($otitle, $ntitle)
336 {
337 $body = $this->getDiffBody();
338 if ($body === false) {
339 return false;
340 } else {
341 $multi = $this->getMultiNotice();
342 return $this->addHeader($body, $otitle, $ntitle, $multi);
343 }
344 }
345
351 public function getDiffBody()
352 {
353 global $wgMemc;
354 $fname = 'DifferenceEngine::getDiffBody';
355 //wfProfileIn( $fname );
356
357 // Cacheable?
358 $key = false;
359 if ($this->mOldid && $this->mNewid) {
360 // Try cache
361 $key = wfMemcKey('diff', 'oldid', $this->mOldid, 'newid', $this->mNewid);
362 $difftext = $wgMemc->get($key);
363 if ($difftext) {
364 wfIncrStats('diff_cache_hit');
365 $difftext = $this->localiseLineNumbers($difftext);
366 $difftext .= "\n<!-- diff cache key $key -->\n";
367 //wfProfileOut( $fname );
368 return $difftext;
369 }
370 }
371
372 #loadtext is permission safe, this just clears out the diff
373 if (!$this->loadText()) {
374 //wfProfileOut( $fname );
375 return false;
376 } elseif ($this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT)) {
377 return '';
378 } elseif ($this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT)) {
379 return '';
380 }
381
382 $difftext = $this->generateDiffBody($this->mOldtext, $this->mNewtext);
383
384 // Save to cache for 7 days
385 if ($key !== false && $difftext !== false) {
386 wfIncrStats('diff_cache_miss');
387 $wgMemc->set($key, $difftext, 7*86400);
388 } else {
389 wfIncrStats('diff_uncacheable');
390 }
391 // Replace line numbers with the text in the user's language
392 if ($difftext !== false) {
393 $difftext = $this->localiseLineNumbers($difftext);
394 }
395 //wfProfileOut( $fname );
396 return $difftext;
397 }
398
403 public function generateDiffBody($otext, $ntext)
404 {
405 global $wgExternalDiffEngine, $wgContLang;
406 $fname = 'DifferenceEngine::generateDiffBody';
407
408 $otext = str_replace("\r\n", "\n", $otext);
409 $ntext = str_replace("\r\n", "\n", $ntext);
410
411 if ($wgExternalDiffEngine == 'wikidiff') {
412 # For historical reasons, external diff engine expects
413 # input text to be HTML-escaped already
414 $otext = htmlspecialchars($wgContLang->segmentForDiff($otext));
415 $ntext = htmlspecialchars($wgContLang->segmentForDiff($ntext));
416 if (!function_exists('wikidiff_do_diff')) {
417 dl('php_wikidiff.so');
418 }
419 return $wgContLang->unsegementForDiff(wikidiff_do_diff($otext, $ntext, 2));
420 }
421
422 if ($wgExternalDiffEngine == 'wikidiff2') {
423 # Better external diff engine, the 2 may some day be dropped
424 # This one does the escaping and segmenting itself
425 if (!function_exists('wikidiff2_do_diff')) {
426 //wfProfileIn( "$fname-dl" );
427 @dl('php_wikidiff2.so');
428 //wfProfileOut( "$fname-dl" );
429 }
430 if (function_exists('wikidiff2_do_diff')) {
431 //wfProfileIn( 'wikidiff2_do_diff' );
432 $text = wikidiff2_do_diff($otext, $ntext, 2);
433 //wfProfileOut( 'wikidiff2_do_diff' );
434 return $text;
435 }
436 }
437 if ($wgExternalDiffEngine !== false) {
438 # Diff via the shell
439 global $wgTmpDirectory;
440 $tempName1 = tempnam($wgTmpDirectory, 'diff_');
441 $tempName2 = tempnam($wgTmpDirectory, 'diff_');
442
443 $tempFile1 = fopen($tempName1, "w");
444 if (!$tempFile1) {
445 //wfProfileOut( $fname );
446 return false;
447 }
448 $tempFile2 = fopen($tempName2, "w");
449 if (!$tempFile2) {
450 //wfProfileOut( $fname );
451 return false;
452 }
453 fwrite($tempFile1, $otext);
454 fwrite($tempFile2, $ntext);
455 fclose($tempFile1);
456 fclose($tempFile2);
457 $cmd = wfEscapeShellArg($wgExternalDiffEngine, $tempName1, $tempName2);
458 //wfProfileIn( "$fname-shellexec" );
459 $difftext = wfShellExec($cmd);
460 //wfProfileOut( "$fname-shellexec" );
461 unlink($tempName1);
462 unlink($tempName2);
463 return $difftext;
464 }
465
466 # Native PHP diff
467 $ota = explode("\n", $wgContLang->segmentForDiff($otext));
468 $nta = explode("\n", $wgContLang->segmentForDiff($ntext));
469 $diffs = new Diff($ota, $nta);
470 $formatter = new TableDiffFormatter();
471 return $wgContLang->unsegmentForDiff($formatter->format($diffs));
472 }
473
474
478 public function localiseLineNumbers($text)
479 {
480 return preg_replace_callback(
481 '/<!--LINE (\d+)-->/',
482 array( &$this, 'localiseLineNumbersCb' ),
483 $text
484 );
485 }
486
487 public function localiseLineNumbersCb($matches)
488 {
489 global $wgLang;
490 return wfMsgExt('lineno', array('parseinline'), $wgLang->formatNum($matches[1]));
491 }
492
493
497 public function getMultiNotice()
498 {
499 if (!is_object($this->mOldRev) || !is_object($this->mNewRev)) {
500 return '';
501 }
502
503 if (!$this->mOldPage->equals($this->mNewPage)) {
504 // Comparing two different pages? Count would be meaningless.
505 return '';
506 }
507
508 $oldid = $this->mOldRev->getId();
509 $newid = $this->mNewRev->getId();
510 if ($oldid > $newid) {
511 $tmp = $oldid;
512 $oldid = $newid;
513 $newid = $tmp;
514 }
515
516 $n = $this->mTitle->countRevisionsBetween($oldid, $newid);
517 if (!$n) {
518 return '';
519 }
520
521 return wfMsgExt('diff-multi', array( 'parseinline' ), $n);
522 }
523
524
528 public function addHeader($diff, $otitle, $ntitle, $multi = '')
529 {
530 global $wgOut;
531
532 if ($this->mOldRev && $this->mOldRev->isDeleted(Revision::DELETED_TEXT)) {
533 $otitle = '<span class="history-deleted">' . $otitle . '</span>';
534 }
535 if ($this->mNewRev && $this->mNewRev->isDeleted(Revision::DELETED_TEXT)) {
536 $ntitle = '<span class="history-deleted">' . $ntitle . '</span>';
537 }
538 $header = "
539 <table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'>
540 <tr>
541 <td colspan='2' width='50%' align='center' class='diff-otitle'>{$otitle}</td>
542 <td colspan='2' width='50%' align='center' class='diff-ntitle'>{$ntitle}</td>
543 </tr>
544 ";
545
546 if ($multi != '') {
547 $header .= "<tr><td colspan='4' align='center' class='diff-multi'>{$multi}</td></tr>";
548 }
549
550 return $header . $diff . "</table>";
551 }
552
556 public function setText($oldText, $newText)
557 {
558 $this->mOldtext = $oldText;
559 $this->mNewtext = $newText;
560 $this->mTextLoaded = 2;
561 }
562
573 public function loadRevisionData()
574 {
575 global $wgLang;
576 if ($this->mRevisionsLoaded) {
577 return true;
578 } else {
579 // Whether it succeeds or fails, we don't want to try again
580 $this->mRevisionsLoaded = true;
581 }
582
583 // Load the new revision object
584 if ($this->mNewid) {
585 $this->mNewRev = Revision::newFromId($this->mNewid);
586 } else {
587 $this->mNewRev = Revision::newFromTitle($this->mTitle);
588 }
589
590 if (is_null($this->mNewRev)) {
591 return false;
592 }
593
594 // Set assorted variables
595 $timestamp = $wgLang->timeanddate($this->mNewRev->getTimestamp(), true);
596 $this->mNewPage = $this->mNewRev->getTitle();
597 if ($this->mNewRev->isCurrent()) {
598 $newLink = $this->mNewPage->escapeLocalUrl();
599 $this->mPagetitle = htmlspecialchars(wfMsg('currentrev'));
600 $newEdit = $this->mNewPage->escapeLocalUrl('action=edit');
601
602 $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a> ($timestamp)"
603 . " (<a href='$newEdit'>" . htmlspecialchars(wfMsg('editold')) . "</a>)";
604 } else {
605 $newLink = $this->mNewPage->escapeLocalUrl('oldid=' . $this->mNewid);
606 $newEdit = $this->mNewPage->escapeLocalUrl('action=edit&oldid=' . $this->mNewid);
607 $this->mPagetitle = htmlspecialchars(wfMsg('revisionasof', $timestamp));
608
609 $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>"
610 . " (<a href='$newEdit'>" . htmlspecialchars(wfMsg('editold')) . "</a>)";
611 }
612
613 // Load the old revision object
614 $this->mOldRev = false;
615 if ($this->mOldid) {
616 $this->mOldRev = Revision::newFromId($this->mOldid);
617 } elseif ($this->mOldid === 0) {
618 $rev = $this->mNewRev->getPrevious();
619 if ($rev) {
620 $this->mOldid = $rev->getId();
621 $this->mOldRev = $rev;
622 } else {
623 // No previous revision; mark to show as first-version only.
624 $this->mOldid = false;
625 $this->mOldRev = false;
626 }
627 }/* elseif ( $this->mOldid === false ) leave mOldRev false; */
628
629 if (is_null($this->mOldRev)) {
630 return false;
631 }
632
633 if ($this->mOldRev) {
634 $this->mOldPage = $this->mOldRev->getTitle();
635
636 $t = $wgLang->timeanddate($this->mOldRev->getTimestamp(), true);
637 $oldLink = $this->mOldPage->escapeLocalUrl('oldid=' . $this->mOldid);
638 $oldEdit = $this->mOldPage->escapeLocalUrl('action=edit&oldid=' . $this->mOldid);
639 $this->mOldtitle = "<a href='$oldLink'>" . htmlspecialchars(wfMsg('revisionasof', $t))
640 . "</a> (<a href='$oldEdit'>" . htmlspecialchars(wfMsg('editold')) . "</a>)";
641 //now that we considered old rev, we can make undo link (bug 8133, multi-edit undo)
642 $newUndo = $this->mNewPage->escapeLocalUrl('action=edit&undoafter=' . $this->mOldid . '&undo=' . $this->mNewid);
643 $this->mNewtitle .= " (<a href='$newUndo'>" . htmlspecialchars(wfMsg('editundo')) . "</a>)";
644 }
645
646 return true;
647 }
648
652 public function loadText()
653 {
654 if ($this->mTextLoaded == 2) {
655 return true;
656 } else {
657 // Whether it succeeds or fails, we don't want to try again
658 $this->mTextLoaded = 2;
659 }
660
661 if (!$this->loadRevisionData()) {
662 return false;
663 }
664 if ($this->mOldRev) {
665 // FIXME: permission tests
666 $this->mOldtext = $this->mOldRev->revText();
667 if ($this->mOldtext === false) {
668 return false;
669 }
670 }
671 if ($this->mNewRev) {
672 $this->mNewtext = $this->mNewRev->revText();
673 if ($this->mNewtext === false) {
674 return false;
675 }
676 }
677 return true;
678 }
679
683 public function loadNewText()
684 {
685 if ($this->mTextLoaded >= 1) {
686 return true;
687 } else {
688 $this->mTextLoaded = 1;
689 }
690 if (!$this->loadRevisionData()) {
691 return false;
692 }
693 $this->mNewtext = $this->mNewRev->getText();
694 return true;
695 }
696}
697
698// A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
699//
700// Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
701// You may copy this code freely under the conditions of the GPL.
702//
703
704define('USE_ASSERTS', function_exists('assert'));
705
712{
713 public $type;
714 public $orig;
715 public $closing;
716
717 public function reverse()
718 {
719 trigger_error('pure virtual', E_USER_ERROR);
720 }
721
722 public function norig()
723 {
724 return $this->orig ? sizeof($this->orig) : 0;
725 }
726
727 public function nclosing()
728 {
729 return $this->closing ? sizeof($this->closing) : 0;
730 }
731}
732
739{
740 public $type = 'copy';
741
742 public function __construct($orig, $closing = false)
743 {
744 if (!is_array($closing)) {
745 $closing = $orig;
746 }
747 $this->orig = $orig;
748 $this->closing = $closing;
749 }
750
751 public function reverse()
752 {
753 return new _DiffOp_Copy($this->closing, $this->orig);
754 }
755}
756
763{
764 public $type = 'delete';
765
766 public function __construct($lines)
767 {
768 $this->orig = $lines;
769 $this->closing = false;
770 }
771
772 public function reverse()
773 {
774 return new _DiffOp_Add($this->orig);
775 }
776}
777
783class _DiffOp_Add extends _DiffOp
784{
785 public $type = 'add';
786
787 public function __construct($lines)
788 {
789 $this->closing = $lines;
790 $this->orig = false;
791 }
792
793 public function reverse()
794 {
795 return new _DiffOp_Delete($this->closing);
796 }
797}
798
805{
806 public $type = 'change';
807
808 public function __construct($orig, $closing)
809 {
810 $this->orig = $orig;
811 $this->closing = $closing;
812 }
813
814 public function reverse()
815 {
816 return new _DiffOp_Change($this->closing, $this->orig);
817 }
818}
819
820
845{
846 const MAX_XREF_LENGTH = 10000;
847
848 public function diff($from_lines, $to_lines)
849 {
850 $fname = '_DiffEngine::diff';
851 //wfProfileIn( $fname );
852
853 $n_from = sizeof($from_lines);
854 $n_to = sizeof($to_lines);
855
856 $this->xchanged = $this->ychanged = array();
857 $this->xv = $this->yv = array();
858 $this->xind = $this->yind = array();
859 unset($this->seq);
860 unset($this->in_seq);
861 unset($this->lcs);
862
863 // Skip leading common lines.
864 for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
865 if ($from_lines[$skip] !== $to_lines[$skip]) {
866 break;
867 }
868 $this->xchanged[$skip] = $this->ychanged[$skip] = false;
869 }
870 // Skip trailing common lines.
871 $xi = $n_from;
872 $yi = $n_to;
873 for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
874 if ($from_lines[$xi] !== $to_lines[$yi]) {
875 break;
876 }
877 $this->xchanged[$xi] = $this->ychanged[$yi] = false;
878 }
879
880 // Ignore lines which do not exist in both files.
881 for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
882 $xhash[$this->_line_hash($from_lines[$xi])] = 1;
883 }
884
885 for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
886 $line = $to_lines[$yi];
887 if (($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)]))) {
888 continue;
889 }
890 $yhash[$this->_line_hash($line)] = 1;
891 $this->yv[] = $line;
892 $this->yind[] = $yi;
893 }
894 for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
895 $line = $from_lines[$xi];
896 if (($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)]))) {
897 continue;
898 }
899 $this->xv[] = $line;
900 $this->xind[] = $xi;
901 }
902
903 // Find the LCS.
904 $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
905
906 // Merge edits when possible
907 $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
908 $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
909
910 // Compute the edit operations.
911 $edits = array();
912 $xi = $yi = 0;
913 while ($xi < $n_from || $yi < $n_to) {
914 USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
915 USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
916
917 // Skip matching "snake".
918 $copy = array();
919 while ($xi < $n_from && $yi < $n_to
920 && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
921 $copy[] = $from_lines[$xi++];
922 ++$yi;
923 }
924 if ($copy) {
925 $edits[] = new _DiffOp_Copy($copy);
926 }
927
928 // Find deletes & adds.
929 $delete = array();
930 while ($xi < $n_from && $this->xchanged[$xi]) {
931 $delete[] = $from_lines[$xi++];
932 }
933
934 $add = array();
935 while ($yi < $n_to && $this->ychanged[$yi]) {
936 $add[] = $to_lines[$yi++];
937 }
938
939 if ($delete && $add) {
940 $edits[] = new _DiffOp_Change($delete, $add);
941 } elseif ($delete) {
942 $edits[] = new _DiffOp_Delete($delete);
943 } elseif ($add) {
944 $edits[] = new _DiffOp_Add($add);
945 }
946 }
947 //wfProfileOut( $fname );
948 return $edits;
949 }
950
954 public function _line_hash($line)
955 {
956 if (strlen($line) > self::MAX_XREF_LENGTH) {
957 return md5($line);
958 } else {
959 return $line;
960 }
961 }
962
963
964 /* Divide the Largest Common Subsequence (LCS) of the sequences
965 * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
966 * sized segments.
967 *
968 * Returns (LCS, PTS). LCS is the length of the LCS. PTS is an
969 * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
970 * sub sequences. The first sub-sequence is contained in [X0, X1),
971 * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on. Note
972 * that (X0, Y0) == (XOFF, YOFF) and
973 * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
974 *
975 * This function assumes that the first lines of the specified portions
976 * of the two files do not match, and likewise that the last lines do not
977 * match. The caller must trim matching lines from the beginning and end
978 * of the portions it is going to specify.
979 */
980 public function _diag($xoff, $xlim, $yoff, $ylim, $nchunks)
981 {
982 $fname = '_DiffEngine::_diag';
983 //wfProfileIn( $fname );
984 $flip = false;
985
986 if ($xlim - $xoff > $ylim - $yoff) {
987 // Things seems faster (I'm not sure I understand why)
988 // when the shortest sequence in X.
989 $flip = true;
990 list($xoff, $xlim, $yoff, $ylim)
991 = array( $yoff, $ylim, $xoff, $xlim);
992 }
993
994 if ($flip) {
995 for ($i = $ylim - 1; $i >= $yoff; $i--) {
996 $ymatches[$this->xv[$i]][] = $i;
997 }
998 } else {
999 for ($i = $ylim - 1; $i >= $yoff; $i--) {
1000 $ymatches[$this->yv[$i]][] = $i;
1001 }
1002 }
1003
1004 $this->lcs = 0;
1005 $this->seq[0]= $yoff - 1;
1006 $this->in_seq = array();
1007 $ymids[0] = array();
1008
1009 $numer = $xlim - $xoff + $nchunks - 1;
1010 $x = $xoff;
1011 for ($chunk = 0; $chunk < $nchunks; $chunk++) {
1012 //wfProfileIn( "$fname-chunk" );
1013 if ($chunk > 0) {
1014 for ($i = 0; $i <= $this->lcs; $i++) {
1015 $ymids[$i][$chunk-1] = $this->seq[$i];
1016 }
1017 }
1018
1019 $x1 = $xoff + (int) (($numer + ($xlim-$xoff)*$chunk) / $nchunks);
1020 for (; $x < $x1; $x++) {
1021 $line = $flip ? $this->yv[$x] : $this->xv[$x];
1022 if (empty($ymatches[$line])) {
1023 continue;
1024 }
1025 $matches = $ymatches[$line];
1026 reset($matches);
1027 while (list($junk, $y) = each($matches)) {
1028 if (empty($this->in_seq[$y])) {
1029 $k = $this->_lcs_pos($y);
1030 USE_ASSERTS && assert($k > 0);
1031 $ymids[$k] = $ymids[$k-1];
1032 break;
1033 }
1034 }
1035 while (list( /* $junk */, $y) = each($matches)) {
1036 if ($y > $this->seq[$k-1]) {
1037 USE_ASSERTS && assert($y < $this->seq[$k]);
1038 // Optimization: this is a common case:
1039 // next match is just replacing previous match.
1040 $this->in_seq[$this->seq[$k]] = false;
1041 $this->seq[$k] = $y;
1042 $this->in_seq[$y] = 1;
1043 } elseif (empty($this->in_seq[$y])) {
1044 $k = $this->_lcs_pos($y);
1045 USE_ASSERTS && assert($k > 0);
1046 $ymids[$k] = $ymids[$k-1];
1047 }
1048 }
1049 }
1050 //wfProfileOut( "$fname-chunk" );
1051 }
1052
1053 $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
1054 $ymid = $ymids[$this->lcs];
1055 for ($n = 0; $n < $nchunks - 1; $n++) {
1056 $x1 = $xoff + (int) (($numer + ($xlim - $xoff) * $n) / $nchunks);
1057 $y1 = $ymid[$n] + 1;
1058 $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
1059 }
1060 $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
1061
1062 //wfProfileOut( $fname );
1063 return array($this->lcs, $seps);
1064 }
1065
1066 public function _lcs_pos($ypos)
1067 {
1068 $fname = '_DiffEngine::_lcs_pos';
1069 //wfProfileIn( $fname );
1070
1071 $end = $this->lcs;
1072 if ($end == 0 || $ypos > $this->seq[$end]) {
1073 $this->seq[++$this->lcs] = $ypos;
1074 $this->in_seq[$ypos] = 1;
1075 //wfProfileOut( $fname );
1076 return $this->lcs;
1077 }
1078
1079 $beg = 1;
1080 while ($beg < $end) {
1081 $mid = (int) (($beg + $end) / 2);
1082 if ($ypos > $this->seq[$mid]) {
1083 $beg = $mid + 1;
1084 } else {
1085 $end = $mid;
1086 }
1087 }
1088
1089 USE_ASSERTS && assert($ypos != $this->seq[$end]);
1090
1091 $this->in_seq[$this->seq[$end]] = false;
1092 $this->seq[$end] = $ypos;
1093 $this->in_seq[$ypos] = 1;
1094 //wfProfileOut( $fname );
1095 return $end;
1096 }
1097
1098 /* Find LCS of two sequences.
1099 *
1100 * The results are recorded in the vectors $this->{x,y}changed[], by
1101 * storing a 1 in the element for each line that is an insertion
1102 * or deletion (ie. is not in the LCS).
1103 *
1104 * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
1105 *
1106 * Note that XLIM, YLIM are exclusive bounds.
1107 * All line numbers are origin-0 and discarded lines are not counted.
1108 */
1109 public function _compareseq($xoff, $xlim, $yoff, $ylim)
1110 {
1111 $fname = '_DiffEngine::_compareseq';
1112 //wfProfileIn( $fname );
1113
1114 // Slide down the bottom initial diagonal.
1115 while ($xoff < $xlim && $yoff < $ylim
1116 && $this->xv[$xoff] == $this->yv[$yoff]) {
1117 ++$xoff;
1118 ++$yoff;
1119 }
1120
1121 // Slide up the top initial diagonal.
1122 while ($xlim > $xoff && $ylim > $yoff
1123 && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
1124 --$xlim;
1125 --$ylim;
1126 }
1127
1128 if ($xoff == $xlim || $yoff == $ylim) {
1129 $lcs = 0;
1130 } else {
1131 // This is ad hoc but seems to work well.
1132 //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
1133 //$nchunks = max(2,min(8,(int)$nchunks));
1134 $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
1135 list($lcs, $seps)
1136 = $this->_diag($xoff, $xlim, $yoff, $ylim, $nchunks);
1137 }
1138
1139 if ($lcs == 0) {
1140 // X and Y sequences have no common subsequence:
1141 // mark all changed.
1142 while ($yoff < $ylim) {
1143 $this->ychanged[$this->yind[$yoff++]] = 1;
1144 }
1145 while ($xoff < $xlim) {
1146 $this->xchanged[$this->xind[$xoff++]] = 1;
1147 }
1148 } else {
1149 // Use the partitions to split this problem into subproblems.
1150 reset($seps);
1151 $pt1 = $seps[0];
1152 while ($pt2 = next($seps)) {
1153 $this->_compareseq($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
1154 $pt1 = $pt2;
1155 }
1156 }
1157 //wfProfileOut( $fname );
1158 }
1159
1160 /* Adjust inserts/deletes of identical lines to join changes
1161 * as much as possible.
1162 *
1163 * We do something when a run of changed lines include a
1164 * line at one end and has an excluded, identical line at the other.
1165 * We are free to choose which identical line is included.
1166 * `compareseq' usually chooses the one at the beginning,
1167 * but usually it is cleaner to consider the following identical line
1168 * to be the "change".
1169 *
1170 * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
1171 */
1172 public function _shift_boundaries($lines, &$changed, $other_changed)
1173 {
1174 $fname = '_DiffEngine::_shift_boundaries';
1175 //wfProfileIn( $fname );
1176 $i = 0;
1177 $j = 0;
1178
1179 USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
1180 $len = sizeof($lines);
1181 $other_len = sizeof($other_changed);
1182
1183 while (1) {
1184 /*
1185 * Scan forwards to find beginning of another run of changes.
1186 * Also keep track of the corresponding point in the other file.
1187 *
1188 * Throughout this code, $i and $j are adjusted together so that
1189 * the first $i elements of $changed and the first $j elements
1190 * of $other_changed both contain the same number of zeros
1191 * (unchanged lines).
1192 * Furthermore, $j is always kept so that $j == $other_len or
1193 * $other_changed[$j] == false.
1194 */
1195 while ($j < $other_len && $other_changed[$j]) {
1196 $j++;
1197 }
1198
1199 while ($i < $len && !$changed[$i]) {
1200 USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
1201 $i++;
1202 $j++;
1203 while ($j < $other_len && $other_changed[$j]) {
1204 $j++;
1205 }
1206 }
1207
1208 if ($i == $len) {
1209 break;
1210 }
1211
1212 $start = $i;
1213
1214 // Find the end of this run of changes.
1215 while (++$i < $len && $changed[$i]) {
1216 continue;
1217 }
1218
1219 do {
1220 /*
1221 * Record the length of this run of changes, so that
1222 * we can later determine whether the run has grown.
1223 */
1224 $runlength = $i - $start;
1225
1226 /*
1227 * Move the changed region back, so long as the
1228 * previous unchanged line matches the last changed one.
1229 * This merges with previous changed regions.
1230 */
1231 while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
1232 $changed[--$start] = 1;
1233 $changed[--$i] = false;
1234 while ($start > 0 && $changed[$start - 1]) {
1235 $start--;
1236 }
1237 USE_ASSERTS && assert('$j > 0');
1238 while ($other_changed[--$j]) {
1239 continue;
1240 }
1241 USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
1242 }
1243
1244 /*
1245 * Set CORRESPONDING to the end of the changed run, at the last
1246 * point where it corresponds to a changed run in the other file.
1247 * CORRESPONDING == LEN means no such point has been found.
1248 */
1249 $corresponding = $j < $other_len ? $i : $len;
1250
1251 /*
1252 * Move the changed region forward, so long as the
1253 * first changed line matches the following unchanged one.
1254 * This merges with following changed regions.
1255 * Do this second, so that if there are no merges,
1256 * the changed region is moved forward as far as possible.
1257 */
1258 while ($i < $len && $lines[$start] == $lines[$i]) {
1259 $changed[$start++] = false;
1260 $changed[$i++] = 1;
1261 while ($i < $len && $changed[$i]) {
1262 $i++;
1263 }
1264
1265 USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
1266 $j++;
1267 if ($j < $other_len && $other_changed[$j]) {
1268 $corresponding = $i;
1269 while ($j < $other_len && $other_changed[$j]) {
1270 $j++;
1271 }
1272 }
1273 }
1274 } while ($runlength != $i - $start);
1275
1276 /*
1277 * If possible, move the fully-merged run of changes
1278 * back to a corresponding run in the other file.
1279 */
1280 while ($corresponding < $i) {
1281 $changed[--$start] = 1;
1282 $changed[--$i] = 0;
1283 USE_ASSERTS && assert('$j > 0');
1284 while ($other_changed[--$j]) {
1285 continue;
1286 }
1287 USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
1288 }
1289 }
1290 //wfProfileOut( $fname );
1291 }
1292}
1293
1300class Diff
1301{
1302 public $edits;
1303
1312 public function __construct($from_lines, $to_lines)
1313 {
1314 $eng = new _DiffEngine;
1315 $this->edits = $eng->diff($from_lines, $to_lines);
1316 //$this->_check($from_lines, $to_lines);
1317 }
1318
1329 public function reverse()
1330 {
1331 $rev = $this;
1332 $rev->edits = array();
1333 foreach ($this->edits as $edit) {
1334 $rev->edits[] = $edit->reverse();
1335 }
1336 return $rev;
1337 }
1338
1344 public function isEmpty()
1345 {
1346 foreach ($this->edits as $edit) {
1347 if ($edit->type != 'copy') {
1348 return false;
1349 }
1350 }
1351 return true;
1352 }
1353
1361 public function lcs()
1362 {
1363 $lcs = 0;
1364 foreach ($this->edits as $edit) {
1365 if ($edit->type == 'copy') {
1366 $lcs += sizeof($edit->orig);
1367 }
1368 }
1369 return $lcs;
1370 }
1371
1380 public function orig()
1381 {
1382 $lines = array();
1383
1384 foreach ($this->edits as $edit) {
1385 if ($edit->orig) {
1386 array_splice($lines, sizeof($lines), 0, $edit->orig);
1387 }
1388 }
1389 return $lines;
1390 }
1391
1400 public function closing()
1401 {
1402 $lines = array();
1403
1404 foreach ($this->edits as $edit) {
1405 if ($edit->closing) {
1406 array_splice($lines, sizeof($lines), 0, $edit->closing);
1407 }
1408 }
1409 return $lines;
1410 }
1411
1417 public function _check($from_lines, $to_lines)
1418 {
1419 $fname = 'Diff::_check';
1420 //wfProfileIn( $fname );
1421 if (serialize($from_lines) != serialize($this->orig())) {
1422 trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
1423 }
1424 if (serialize($to_lines) != serialize($this->closing())) {
1425 trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
1426 }
1427
1428 $rev = $this->reverse();
1429 if (serialize($to_lines) != serialize($rev->orig())) {
1430 trigger_error("Reversed original doesn't match", E_USER_ERROR);
1431 }
1432 if (serialize($from_lines) != serialize($rev->closing())) {
1433 trigger_error("Reversed closing doesn't match", E_USER_ERROR);
1434 }
1435
1436
1437 $prevtype = 'none';
1438 foreach ($this->edits as $edit) {
1439 if ($prevtype == $edit->type) {
1440 trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
1441 }
1442 $prevtype = $edit->type;
1443 }
1444
1445 $lcs = $this->lcs();
1446 trigger_error('Diff okay: LCS = ' . $lcs, E_USER_NOTICE);
1447 //wfProfileOut( $fname );
1448 }
1449}
1450
1456class MappedDiff extends Diff
1457{
1481 public function __construct(
1482 $from_lines,
1483 $to_lines,
1484 $mapped_from_lines,
1485 $mapped_to_lines
1486 ) {
1487 $fname = 'MappedDiff::MappedDiff';
1488 //wfProfileIn( $fname );
1489
1490 assert(sizeof($from_lines) == sizeof($mapped_from_lines));
1491 assert(sizeof($to_lines) == sizeof($mapped_to_lines));
1492
1493 parent::__construct($mapped_from_lines, $mapped_to_lines);
1494
1495 $xi = $yi = 0;
1496 for ($i = 0; $i < sizeof($this->edits); $i++) {
1497 $orig = &$this->edits[$i]->orig;
1498 if (is_array($orig)) {
1499 $orig = array_slice($from_lines, $xi, sizeof($orig));
1500 $xi += sizeof($orig);
1501 }
1502
1503 $closing = &$this->edits[$i]->closing;
1504 if (is_array($closing)) {
1505 $closing = array_slice($to_lines, $yi, sizeof($closing));
1506 $yi += sizeof($closing);
1507 }
1508 }
1509 //wfProfileOut( $fname );
1510 }
1511}
1512
1524{
1532
1540
1547 public function format($diff)
1548 {
1549 $fname = 'DiffFormatter::format';
1550 //wfProfileIn( $fname );
1551
1552 $xi = $yi = 1;
1553 $block = false;
1554 $context = array();
1555
1558
1559 $this->_start_diff();
1560
1561 foreach ($diff->edits as $edit) {
1562 if ($edit->type == 'copy') {
1563 if (is_array($block)) {
1564 if (sizeof($edit->orig) <= $nlead + $ntrail) {
1565 $block[] = $edit;
1566 } else {
1567 if ($ntrail) {
1568 $context = array_slice($edit->orig, 0, $ntrail);
1569 $block[] = new _DiffOp_Copy($context);
1570 }
1571 $this->_block(
1572 $x0,
1573 $ntrail + $xi - $x0,
1574 $y0,
1575 $ntrail + $yi - $y0,
1576 $block
1577 );
1578 $block = false;
1579 }
1580 }
1581 $context = $edit->orig;
1582 } else {
1583 if (!is_array($block)) {
1584 $context = array_slice($context, sizeof($context) - $nlead);
1585 $x0 = $xi - sizeof($context);
1586 $y0 = $yi - sizeof($context);
1587 $block = array();
1588 if ($context) {
1589 $block[] = new _DiffOp_Copy($context);
1590 }
1591 }
1592 $block[] = $edit;
1593 }
1594
1595 if ($edit->orig) {
1596 $xi += sizeof($edit->orig);
1597 }
1598 if ($edit->closing) {
1599 $yi += sizeof($edit->closing);
1600 }
1601 }
1602
1603 if (is_array($block)) {
1604 $this->_block(
1605 $x0,
1606 $xi - $x0,
1607 $y0,
1608 $yi - $y0,
1609 $block
1610 );
1611 }
1612
1613 $end = $this->_end_diff();
1614 //wfProfileOut( $fname );
1615 return $end;
1616 }
1617
1618 public function _block($xbeg, $xlen, $ybeg, $ylen, &$edits)
1619 {
1620 $fname = 'DiffFormatter::_block';
1621 //wfProfileIn( $fname );
1622 $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
1623 foreach ($edits as $edit) {
1624 if ($edit->type == 'copy') {
1625 $this->_context($edit->orig);
1626 } elseif ($edit->type == 'add') {
1627 $this->_added($edit->closing);
1628 } elseif ($edit->type == 'delete') {
1629 $this->_deleted($edit->orig);
1630 } elseif ($edit->type == 'change') {
1631 $this->_changed($edit->orig, $edit->closing);
1632 } else {
1633 trigger_error('Unknown edit type', E_USER_ERROR);
1634 }
1635 }
1636 $this->_end_block();
1637 //wfProfileOut( $fname );
1638 }
1639
1640 public function _start_diff()
1641 {
1642 ob_start();
1643 }
1644
1645 public function _end_diff()
1646 {
1647 $val = ob_get_contents();
1648 ob_end_clean();
1649 return $val;
1650 }
1651
1652 public function _block_header($xbeg, $xlen, $ybeg, $ylen)
1653 {
1654 if ($xlen > 1) {
1655 $xbeg .= "," . ($xbeg + $xlen - 1);
1656 }
1657 if ($ylen > 1) {
1658 $ybeg .= "," . ($ybeg + $ylen - 1);
1659 }
1660
1661 return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
1662 }
1663
1664 public function _start_block($header)
1665 {
1666 echo $header;
1667 }
1668
1669 public function _end_block()
1670 {
1671 }
1672
1673 public function _lines($lines, $prefix = ' ')
1674 {
1675 foreach ($lines as $line) {
1676 echo "$prefix $line\n";
1677 }
1678 }
1679
1680 public function _context($lines)
1681 {
1682 $this->_lines($lines);
1683 }
1684
1685 public function _added($lines)
1686 {
1687 $this->_lines($lines, '>');
1688 }
1689 public function _deleted($lines)
1690 {
1691 $this->_lines($lines, '<');
1692 }
1693
1694 public function _changed($orig, $closing)
1695 {
1696 $this->_deleted($orig);
1697 echo "---\n";
1698 $this->_added($closing);
1699 }
1700}
1701
1702
1708define('NBSP', '&#160;'); // iso-8859-x non-breaking space.
1709
1716{
1717 public function __construct()
1718 {
1719 $this->_lines = array();
1720 $this->_line = '';
1721 $this->_group = '';
1722 $this->_tag = '';
1723 }
1724
1725 public function _flushGroup($new_tag)
1726 {
1727 if ($this->_group !== '') {
1728 if ($this->_tag == 'ins') {
1729 $this->_line .= '[ilDiffInsStart]' .
1730 htmlspecialchars($this->_group) . '[ilDiffInsEnd]';
1731 } elseif ($this->_tag == 'del') {
1732 $this->_line .= '[ilDiffDelStart]' .
1733 htmlspecialchars($this->_group) . '[ilDiffDelEnd]';
1734 } else {
1735 $this->_line .= htmlspecialchars($this->_group);
1736 }
1737 }
1738 $this->_group = '';
1739 $this->_tag = $new_tag;
1740 }
1741
1742 public function _flushLine($new_tag)
1743 {
1744 $this->_flushGroup($new_tag);
1745 if ($this->_line != '') {
1746 array_push($this->_lines, $this->_line);
1747 } else {
1748 # make empty lines visible by inserting an NBSP
1749 array_push($this->_lines, NBSP);
1750 }
1751 $this->_line = '';
1752 }
1753
1754 public function addWords($words, $tag = '')
1755 {
1756 if ($tag != $this->_tag) {
1757 $this->_flushGroup($tag);
1758 }
1759
1760 foreach ($words as $word) {
1761 // new-line should only come as first char of word.
1762 if ($word == '') {
1763 continue;
1764 }
1765 if ($word[0] == "\n") {
1766 $this->_flushLine($tag);
1767 $word = substr($word, 1);
1768 }
1769 assert(!strstr($word, "\n"));
1770 $this->_group .= $word;
1771 }
1772 }
1773
1774 public function getLines()
1775 {
1776 $this->_flushLine('~done');
1777 return $this->_lines;
1778 }
1779}
1780
1787{
1788 const MAX_LINE_LENGTH = 10000;
1789
1790 public function __construct($orig_lines, $closing_lines)
1791 {
1792 $fname = 'WordLevelDiff::WordLevelDiff';
1793 //wfProfileIn( $fname );
1794
1795 list($orig_words, $orig_stripped) = $this->_split($orig_lines);
1796 list($closing_words, $closing_stripped) = $this->_split($closing_lines);
1797
1798 parent::__construct(
1799 $orig_words,
1800 $closing_words,
1801 $orig_stripped,
1802 $closing_stripped
1803 );
1804 //wfProfileOut( $fname );
1805 }
1806
1807 public function _split($lines)
1808 {
1809 $fname = 'WordLevelDiff::_split';
1810 //wfProfileIn( $fname );
1811
1812 $words = array();
1813 $stripped = array();
1814 $first = true;
1815 foreach ($lines as $line) {
1816 # If the line is too long, just pretend the entire line is one big word
1817 # This prevents resource exhaustion problems
1818 if ($first) {
1819 $first = false;
1820 } else {
1821 $words[] = "\n";
1822 $stripped[] = "\n";
1823 }
1824 if (strlen($line) > self::MAX_LINE_LENGTH) {
1825 $words[] = $line;
1826 $stripped[] = $line;
1827 } else {
1828 $m = array();
1829 if (preg_match_all(
1830 '/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs',
1831 $line,
1832 $m
1833 )) {
1834 $words = array_merge($words, $m[0]);
1835 $stripped = array_merge($stripped, $m[1]);
1836 }
1837 }
1838 }
1839 //wfProfileOut( $fname );
1840 return array($words, $stripped);
1841 }
1842
1843 public function orig()
1844 {
1845 $fname = 'WordLevelDiff::orig';
1846 //wfProfileIn( $fname );
1848
1849 foreach ($this->edits as $edit) {
1850 if ($edit->type == 'copy') {
1851 $orig->addWords($edit->orig);
1852 } elseif ($edit->orig) {
1853 $orig->addWords($edit->orig, 'del');
1854 }
1855 }
1856 $lines = $orig->getLines();
1857 //wfProfileOut( $fname );
1858 return $lines;
1859 }
1860
1861 public function closing()
1862 {
1863 $fname = 'WordLevelDiff::closing';
1864 //wfProfileIn( $fname );
1865 $closing = new _HWLDF_WordAccumulator;
1866
1867 foreach ($this->edits as $edit) {
1868 if ($edit->type == 'copy') {
1869 $closing->addWords($edit->closing);
1870 } elseif ($edit->closing) {
1871 $closing->addWords($edit->closing, 'ins');
1872 }
1873 }
1874 $lines = $closing->getLines();
1875 //wfProfileOut( $fname );
1876 return $lines;
1877 }
1878}
1879
1887{
1888 public function __construct()
1889 {
1890 $this->leading_context_lines = 2;
1891 $this->trailing_context_lines = 2;
1892 }
1893
1894 public function _block_header($xbeg, $xlen, $ybeg, $ylen)
1895 {
1896 $r = '<tr><td colspan="2" align="left"><strong><!--LINE ' . $xbeg . "--></strong></td>\n" .
1897 '<td colspan="2" align="left"><strong><!--LINE ' . $ybeg . "--></strong></td></tr>\n";
1898 return $r;
1899 }
1900
1901 public function _start_block($header)
1902 {
1903 echo $header;
1904 }
1905
1906 public function _end_block()
1907 {
1908 }
1909
1910 public function _lines($lines, $prefix=' ', $color='white')
1911 {
1912 }
1913
1914 # HTML-escape parameter before calling this
1915 public function addedLine($line)
1916 {
1917 return "<td>+</td><td class='diff-addedline'>{$line}</td>";
1918 }
1919
1920 # HTML-escape parameter before calling this
1921 public function deletedLine($line)
1922 {
1923 return "<td>-</td><td class='diff-deletedline'>{$line}</td>";
1924 }
1925
1926 # HTML-escape parameter before calling this
1927 public function contextLine($line)
1928 {
1929 return "<td> </td><td class='diff-context'>{$line}</td>";
1930 }
1931
1932 public function emptyLine()
1933 {
1934 return '<td colspan="2">&nbsp;</td>';
1935 }
1936
1937 public function _added($lines)
1938 {
1939 foreach ($lines as $line) {
1940 echo '<tr>' . $this->emptyLine() .
1941 $this->addedLine(htmlspecialchars($line)) . "</tr>\n";
1942 }
1943 }
1944
1945 public function _deleted($lines)
1946 {
1947 foreach ($lines as $line) {
1948 echo '<tr>' . $this->deletedLine(htmlspecialchars($line)) .
1949 $this->emptyLine() . "</tr>\n";
1950 }
1951 }
1952
1953 public function _context($lines)
1954 {
1955 foreach ($lines as $line) {
1956 echo '<tr>' .
1957 $this->contextLine(htmlspecialchars($line)) .
1958 $this->contextLine(htmlspecialchars($line)) . "</tr>\n";
1959 }
1960 }
1961
1962 public function _changed($orig, $closing)
1963 {
1964 $fname = 'TableDiffFormatter::_changed';
1965 //wfProfileIn( $fname );
1966
1967 $diff = new WordLevelDiff($orig, $closing);
1968 $del = $diff->orig();
1969 $add = $diff->closing();
1970
1971 # Notice that WordLevelDiff returns HTML-escaped output.
1972 # Hence, we will be calling addedLine/deletedLine without HTML-escaping.
1973
1974 while ($line = array_shift($del)) {
1975 $aline = array_shift($add);
1976 echo '<tr>' . $this->deletedLine($line) .
1977 $this->addedLine($aline) . "</tr>\n";
1978 }
1979 foreach ($add as $line) { # If any leftovers
1980 echo '<tr>' . $this->emptyLine() .
1981 $this->addedLine($line) . "</tr>\n";
1982 }
1983 //wfProfileOut( $fname );
1984 }
1985}
const NS_SPECIAL
Definition: Title.php:16
wfMsg($x)
Definition: RandomTest.php:63
$n
Definition: RandomTest.php:85
foreach($mandatory_scripts as $file) $timestamp
Definition: buildRTE.php:81
An exception for terminatinating execution or to throw for unit testing.
_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.
__construct($from_lines, $to_lines)
Constructor.
isEmpty()
Check for empty diff.
lcs()
Compute the length of the Longest Common Subsequence (LCS).
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.
loadText()
Load the text of the revisions, as well as revision data.
addHeader($diff, $otitle, $ntitle, $multi='')
Add the header to a diff body.
getDiffBody()
Get the diff table body, without header Results are cached Returns false on error.
localiseLineNumbers($text)
Replace line numbers with the text in the user's language.
loadRevisionData()
Load revision metadata for the specified articles.
showDiff($otitle, $ntitle)
Get the diff text, send it to $wgOut Returns false if the diff could not be generated,...
getMultiNotice()
If there are revisions between the ones being compared, return a note saying so.
__construct($titleObj=null, $old=0, $new=0, $rcid=0)
#-
generateDiffBody($otext, $ntext)
Generate a diff, no caching $otext and $ntext must be already segmented.
showDiffPage($diffOnly=false)
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.
setText($oldText, $newText)
Use specified text instead of loading from the database.
__construct( $from_lines, $to_lines, $mapped_from_lines, $mapped_to_lines)
Constructor.
_block_header($xbeg, $xlen, $ybeg, $ylen)
_lines($lines, $prefix=' ', $color='white')
_changed($orig, $closing)
__construct($orig_lines, $closing_lines)
Constructor.
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)
diff($from_lines, $to_lines)
_shift_boundaries($lines, &$changed, $other_changed)
_line_hash($line)
Returns the whole line if it's small enough, or the MD5 hash otherwise.
_compareseq($xoff, $xlim, $yoff, $ylim)
__construct($orig, $closing)
__construct($orig, $closing=false)
$key
Definition: croninfo.php:18
$i
Definition: disco.tpl.php:19
$y
Definition: example_007.php:83
$x
Definition: example_009.php:98
$r
Definition: example_031.php:79
if(function_exists( 'posix_getuid') &&posix_getuid()===0) if(!array_key_exists('t', $options)) $tag
Definition: cron.php:35
$end
Definition: saml1-acs.php:18
$old
$text
Definition: errorreport.php:18