ILIAS  release_8 Revision v8.24
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 public 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 foreach ($matches as $junk => $y) {
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 foreach ($matches as $y) {
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 }
1217
1218 do {
1219 /*
1220 * Record the length of this run of changes, so that
1221 * we can later determine whether the run has grown.
1222 */
1223 $runlength = $i - $start;
1224
1225 /*
1226 * Move the changed region back, so long as the
1227 * previous unchanged line matches the last changed one.
1228 * This merges with previous changed regions.
1229 */
1230 while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
1231 $changed[--$start] = 1;
1232 $changed[--$i] = false;
1233 while ($start > 0 && $changed[$start - 1]) {
1234 $start--;
1235 }
1236 USE_ASSERTS && assert($j > 0);
1237 while ($other_changed[--$j]) {
1238 }
1239 USE_ASSERTS && assert($j >= 0 && !$other_changed[$j]);
1240 }
1241
1242 /*
1243 * Set CORRESPONDING to the end of the changed run, at the last
1244 * point where it corresponds to a changed run in the other file.
1245 * CORRESPONDING == LEN means no such point has been found.
1246 */
1247 $corresponding = $j < $other_len ? $i : $len;
1248
1249 /*
1250 * Move the changed region forward, so long as the
1251 * first changed line matches the following unchanged one.
1252 * This merges with following changed regions.
1253 * Do this second, so that if there are no merges,
1254 * the changed region is moved forward as far as possible.
1255 */
1256 while ($i < $len && $lines[$start] == $lines[$i]) {
1257 $changed[$start++] = false;
1258 $changed[$i++] = 1;
1259 while ($i < $len && $changed[$i]) {
1260 $i++;
1261 }
1262
1263 USE_ASSERTS && assert($j < $other_len && !$other_changed[$j]);
1264 $j++;
1265 if ($j < $other_len && $other_changed[$j]) {
1266 $corresponding = $i;
1267 while ($j < $other_len && $other_changed[$j]) {
1268 $j++;
1269 }
1270 }
1271 }
1272 } while ($runlength != $i - $start);
1273
1274 /*
1275 * If possible, move the fully-merged run of changes
1276 * back to a corresponding run in the other file.
1277 */
1278 while ($corresponding < $i) {
1279 $changed[--$start] = 1;
1280 $changed[--$i] = 0;
1281 USE_ASSERTS && assert($j > 0);
1282 while ($other_changed[--$j]) {
1283 }
1284 USE_ASSERTS && assert($j >= 0 && !$other_changed[$j]);
1285 }
1286 }
1287 //wfProfileOut( $fname );
1288 }
1289}
1290
1297class Diff
1298{
1299 public $edits;
1300
1309 public function __construct($from_lines, $to_lines)
1310 {
1311 $eng = new _DiffEngine();
1312 $this->edits = $eng->diff($from_lines, $to_lines);
1313 //$this->_check($from_lines, $to_lines);
1314 }
1315
1326 public function reverse()
1327 {
1328 $rev = $this;
1329 $rev->edits = array();
1330 foreach ($this->edits as $edit) {
1331 $rev->edits[] = $edit->reverse();
1332 }
1333 return $rev;
1334 }
1335
1341 public function isEmpty()
1342 {
1343 foreach ($this->edits as $edit) {
1344 if ($edit->type != 'copy') {
1345 return false;
1346 }
1347 }
1348 return true;
1349 }
1350
1358 public function lcs()
1359 {
1360 $lcs = 0;
1361 foreach ($this->edits as $edit) {
1362 if ($edit->type == 'copy') {
1363 $lcs += sizeof($edit->orig);
1364 }
1365 }
1366 return $lcs;
1367 }
1368
1377 public function orig()
1378 {
1379 $lines = array();
1380
1381 foreach ($this->edits as $edit) {
1382 if ($edit->orig) {
1383 array_splice($lines, sizeof($lines), 0, $edit->orig);
1384 }
1385 }
1386 return $lines;
1387 }
1388
1397 public function closing()
1398 {
1399 $lines = array();
1400
1401 foreach ($this->edits as $edit) {
1402 if ($edit->closing) {
1403 array_splice($lines, sizeof($lines), 0, $edit->closing);
1404 }
1405 }
1406 return $lines;
1407 }
1408
1414 public function _check($from_lines, $to_lines)
1415 {
1416 $fname = 'Diff::_check';
1417 //wfProfileIn( $fname );
1418 if (serialize($from_lines) != serialize($this->orig())) {
1419 trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
1420 }
1421 if (serialize($to_lines) != serialize($this->closing())) {
1422 trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
1423 }
1424
1425 $rev = $this->reverse();
1426 if (serialize($to_lines) != serialize($rev->orig())) {
1427 trigger_error("Reversed original doesn't match", E_USER_ERROR);
1428 }
1429 if (serialize($from_lines) != serialize($rev->closing())) {
1430 trigger_error("Reversed closing doesn't match", E_USER_ERROR);
1431 }
1432
1433
1434 $prevtype = 'none';
1435 foreach ($this->edits as $edit) {
1436 if ($prevtype == $edit->type) {
1437 trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
1438 }
1439 $prevtype = $edit->type;
1440 }
1441
1442 $lcs = $this->lcs();
1443 trigger_error('Diff okay: LCS = ' . $lcs, E_USER_NOTICE);
1444 //wfProfileOut( $fname );
1445 }
1446}
1447
1453class MappedDiff extends Diff
1454{
1478 public function __construct(
1479 $from_lines,
1480 $to_lines,
1481 $mapped_from_lines,
1482 $mapped_to_lines
1483 ) {
1484 $fname = 'MappedDiff::MappedDiff';
1485 //wfProfileIn( $fname );
1486
1487 assert(sizeof($from_lines) == sizeof($mapped_from_lines));
1488 assert(sizeof($to_lines) == sizeof($mapped_to_lines));
1489
1490 parent::__construct($mapped_from_lines, $mapped_to_lines);
1491
1492 $xi = $yi = 0;
1493 for ($i = 0; $i < sizeof($this->edits); $i++) {
1494 $orig = &$this->edits[$i]->orig;
1495 if (is_array($orig)) {
1496 $orig = array_slice($from_lines, $xi, sizeof($orig));
1497 $xi += sizeof($orig);
1498 }
1499
1500 $closing = &$this->edits[$i]->closing;
1501 if (is_array($closing)) {
1502 $closing = array_slice($to_lines, $yi, sizeof($closing));
1503 $yi += sizeof($closing);
1504 }
1505 }
1506 //wfProfileOut( $fname );
1507 }
1508}
1509
1521{
1529
1537
1544 public function format($diff)
1545 {
1546 $fname = 'DiffFormatter::format';
1547 //wfProfileIn( $fname );
1548
1549 $xi = $yi = 1;
1550 $block = false;
1551 $context = array();
1552
1555
1556 $this->_start_diff();
1557
1558 foreach ($diff->edits as $edit) {
1559 if ($edit->type == 'copy') {
1560 if (is_array($block)) {
1561 if (sizeof($edit->orig) <= $nlead + $ntrail) {
1562 $block[] = $edit;
1563 } else {
1564 if ($ntrail) {
1565 $context = array_slice($edit->orig, 0, $ntrail);
1566 $block[] = new _DiffOp_Copy($context);
1567 }
1568 $this->_block(
1569 $x0,
1570 $ntrail + $xi - $x0,
1571 $y0,
1572 $ntrail + $yi - $y0,
1573 $block
1574 );
1575 $block = false;
1576 }
1577 }
1578 $context = $edit->orig;
1579 } else {
1580 if (!is_array($block)) {
1581 $context = array_slice($context, sizeof($context) - $nlead);
1582 $x0 = $xi - sizeof($context);
1583 $y0 = $yi - sizeof($context);
1584 $block = array();
1585 if ($context) {
1586 $block[] = new _DiffOp_Copy($context);
1587 }
1588 }
1589 $block[] = $edit;
1590 }
1591
1592 if ($edit->orig) {
1593 $xi += sizeof($edit->orig);
1594 }
1595 if ($edit->closing) {
1596 $yi += sizeof($edit->closing);
1597 }
1598 }
1599
1600 if (is_array($block)) {
1601 $this->_block(
1602 $x0,
1603 $xi - $x0,
1604 $y0,
1605 $yi - $y0,
1606 $block
1607 );
1608 }
1609
1610 $end = $this->_end_diff();
1611 //wfProfileOut( $fname );
1612 return $end;
1613 }
1614
1615 public function _block($xbeg, $xlen, $ybeg, $ylen, $edits)
1616 {
1617 $fname = 'DiffFormatter::_block';
1618 //wfProfileIn( $fname );
1619 $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
1620 foreach ($edits as $edit) {
1621 if ($edit->type == 'copy') {
1622 $this->_context($edit->orig);
1623 } elseif ($edit->type == 'add') {
1624 $this->_added($edit->closing);
1625 } elseif ($edit->type == 'delete') {
1626 $this->_deleted($edit->orig);
1627 } elseif ($edit->type == 'change') {
1628 $this->_changed($edit->orig, $edit->closing);
1629 } else {
1630 trigger_error('Unknown edit type', E_USER_ERROR);
1631 }
1632 }
1633 $this->_end_block();
1634 //wfProfileOut( $fname );
1635 }
1636
1637 public function _start_diff()
1638 {
1639 ob_start();
1640 }
1641
1642 public function _end_diff()
1643 {
1644 $val = ob_get_contents();
1645 ob_end_clean();
1646 return $val;
1647 }
1648
1649 public function _block_header($xbeg, $xlen, $ybeg, $ylen)
1650 {
1651 if ($xlen > 1) {
1652 $xbeg .= "," . ($xbeg + $xlen - 1);
1653 }
1654 if ($ylen > 1) {
1655 $ybeg .= "," . ($ybeg + $ylen - 1);
1656 }
1657
1658 return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
1659 }
1660
1661 public function _start_block($header)
1662 {
1663 echo $header;
1664 }
1665
1666 public function _end_block()
1667 {
1668 }
1669
1670 public function _lines($lines, $prefix = ' ')
1671 {
1672 foreach ($lines as $line) {
1673 echo "$prefix $line\n";
1674 }
1675 }
1676
1677 public function _context($lines)
1678 {
1679 $this->_lines($lines);
1680 }
1681
1682 public function _added($lines)
1683 {
1684 $this->_lines($lines, '>');
1685 }
1686 public function _deleted($lines)
1687 {
1688 $this->_lines($lines, '<');
1689 }
1690
1691 public function _changed($orig, $closing)
1692 {
1693 $this->_deleted($orig);
1694 echo "---\n";
1695 $this->_added($closing);
1696 }
1697}
1698
1699
1705define('NBSP', '&#160;'); // iso-8859-x non-breaking space.
1706
1713{
1714 public function __construct()
1715 {
1716 $this->_lines = array();
1717 $this->_line = '';
1718 $this->_group = '';
1719 $this->_tag = '';
1720 }
1721
1722 public function _flushGroup($new_tag)
1723 {
1724 if ($this->_group !== '') {
1725 if ($this->_tag == 'ins') {
1726 $this->_line .= '[ilDiffInsStart]' .
1727 htmlspecialchars($this->_group) . '[ilDiffInsEnd]';
1728 } elseif ($this->_tag == 'del') {
1729 $this->_line .= '[ilDiffDelStart]' .
1730 htmlspecialchars($this->_group) . '[ilDiffDelEnd]';
1731 } else {
1732 $this->_line .= htmlspecialchars($this->_group);
1733 }
1734 }
1735 $this->_group = '';
1736 $this->_tag = $new_tag;
1737 }
1738
1739 public function _flushLine($new_tag)
1740 {
1741 $this->_flushGroup($new_tag);
1742 if ($this->_line != '') {
1743 array_push($this->_lines, $this->_line);
1744 } else {
1745 # make empty lines visible by inserting an NBSP
1746 array_push($this->_lines, NBSP);
1747 }
1748 $this->_line = '';
1749 }
1750
1751 public function addWords($words, $tag = '')
1752 {
1753 if ($tag != $this->_tag) {
1754 $this->_flushGroup($tag);
1755 }
1756
1757 foreach ($words as $word) {
1758 // new-line should only come as first char of word.
1759 if ($word == '') {
1760 continue;
1761 }
1762 if ($word[0] == "\n") {
1763 $this->_flushLine($tag);
1764 $word = substr($word, 1);
1765 }
1766 assert(!strstr($word, "\n"));
1767 $this->_group .= $word;
1768 }
1769 }
1770
1771 public function getLines()
1772 {
1773 $this->_flushLine('~done');
1774 return $this->_lines;
1775 }
1776}
1777
1784{
1785 public const MAX_LINE_LENGTH = 10000;
1786
1787 public function __construct($orig_lines, $closing_lines)
1788 {
1789 $fname = 'WordLevelDiff::WordLevelDiff';
1790 //wfProfileIn( $fname );
1791
1792 list($orig_words, $orig_stripped) = $this->_split($orig_lines);
1793 list($closing_words, $closing_stripped) = $this->_split($closing_lines);
1794
1796 $orig_words,
1797 $closing_words,
1798 $orig_stripped,
1799 $closing_stripped
1800 );
1801 //wfProfileOut( $fname );
1802 }
1803
1804 public function _split($lines)
1805 {
1806 $fname = 'WordLevelDiff::_split';
1807 //wfProfileIn( $fname );
1808
1809 $words = array();
1810 $stripped = array();
1811 $first = true;
1812 foreach ($lines as $line) {
1813 # If the line is too long, just pretend the entire line is one big word
1814 # This prevents resource exhaustion problems
1815 if ($first) {
1816 $first = false;
1817 } else {
1818 $words[] = "\n";
1819 $stripped[] = "\n";
1820 }
1821 if (strlen($line) > self::MAX_LINE_LENGTH) {
1822 $words[] = $line;
1823 $stripped[] = $line;
1824 } else {
1825 $m = array();
1826 if (preg_match_all(
1827 '/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs',
1828 $line,
1829 $m
1830 )) {
1831 $words = array_merge($words, $m[0]);
1832 $stripped = array_merge($stripped, $m[1]);
1833 }
1834 }
1835 }
1836 //wfProfileOut( $fname );
1837 return array($words, $stripped);
1838 }
1839
1840 public function orig()
1841 {
1842 $fname = 'WordLevelDiff::orig';
1843 //wfProfileIn( $fname );
1844 $orig = new _HWLDF_WordAccumulator();
1845
1846 foreach ($this->edits as $edit) {
1847 if ($edit->type == 'copy') {
1848 $orig->addWords($edit->orig);
1849 } elseif ($edit->orig) {
1850 $orig->addWords($edit->orig, 'del');
1851 }
1852 }
1853 $lines = $orig->getLines();
1854 //wfProfileOut( $fname );
1855 return $lines;
1856 }
1857
1858 public function closing()
1859 {
1860 $fname = 'WordLevelDiff::closing';
1861 //wfProfileIn( $fname );
1862 $closing = new _HWLDF_WordAccumulator();
1863
1864 foreach ($this->edits as $edit) {
1865 if ($edit->type == 'copy') {
1866 $closing->addWords($edit->closing);
1867 } elseif ($edit->closing) {
1868 $closing->addWords($edit->closing, 'ins');
1869 }
1870 }
1871 $lines = $closing->getLines();
1872 //wfProfileOut( $fname );
1873 return $lines;
1874 }
1875}
1876
1884{
1885 public function __construct()
1886 {
1887 $this->leading_context_lines = 2;
1888 $this->trailing_context_lines = 2;
1889 }
1890
1891 public function _block_header($xbeg, $xlen, $ybeg, $ylen)
1892 {
1893 $r = '<tr><td colspan="2" align="left"><strong><!--LINE ' . $xbeg . "--></strong></td>\n" .
1894 '<td colspan="2" align="left"><strong><!--LINE ' . $ybeg . "--></strong></td></tr>\n";
1895 return $r;
1896 }
1897
1898 public function _start_block($header)
1899 {
1900 echo $header;
1901 }
1902
1903 public function _end_block()
1904 {
1905 }
1906
1907 public function _lines($lines, $prefix = ' ', $color = 'white')
1908 {
1909 }
1910
1911 # HTML-escape parameter before calling this
1912 public function addedLine($line)
1913 {
1914 return "<td>+</td><td class='diff-addedline'>{$line}</td>";
1915 }
1916
1917 # HTML-escape parameter before calling this
1918 public function deletedLine($line)
1919 {
1920 return "<td>-</td><td class='diff-deletedline'>{$line}</td>";
1921 }
1922
1923 # HTML-escape parameter before calling this
1924 public function contextLine($line)
1925 {
1926 return "<td> </td><td class='diff-context'>{$line}</td>";
1927 }
1928
1929 public function emptyLine()
1930 {
1931 return '<td colspan="2">&nbsp;</td>';
1932 }
1933
1934 public function _added($lines)
1935 {
1936 foreach ($lines as $line) {
1937 echo '<tr>' . $this->emptyLine() .
1938 $this->addedLine(htmlspecialchars($line)) . "</tr>\n";
1939 }
1940 }
1941
1942 public function _deleted($lines)
1943 {
1944 foreach ($lines as $line) {
1945 echo '<tr>' . $this->deletedLine(htmlspecialchars($line)) .
1946 $this->emptyLine() . "</tr>\n";
1947 }
1948 }
1949
1950 public function _context($lines)
1951 {
1952 foreach ($lines as $line) {
1953 echo '<tr>' .
1954 $this->contextLine(htmlspecialchars($line)) .
1955 $this->contextLine(htmlspecialchars($line)) . "</tr>\n";
1956 }
1957 }
1958
1959 public function _changed($orig, $closing)
1960 {
1961 $fname = 'TableDiffFormatter::_changed';
1962 //wfProfileIn( $fname );
1963
1964 $diff = new WordLevelDiff($orig, $closing);
1965 $del = $diff->orig();
1966 $add = $diff->closing();
1967
1968 # Notice that WordLevelDiff returns HTML-escaped output.
1969 # Hence, we will be calling addedLine/deletedLine without HTML-escaping.
1970
1971 while ($line = array_shift($del)) {
1972 $aline = array_shift($add);
1973 echo '<tr>' . $this->deletedLine($line) .
1974 $this->addedLine($aline) . "</tr>\n";
1975 }
1976 foreach ($add as $line) { # If any leftovers
1977 echo '<tr>' . $this->emptyLine() .
1978 $this->addedLine($line) . "</tr>\n";
1979 }
1980 //wfProfileOut( $fname );
1981 }
1982}
const NS_SPECIAL
Definition: Title.php:6
foreach($mandatory_scripts as $file) $timestamp
Definition: buildRTE.php:70
_block_header($xbeg, $xlen, $ybeg, $ylen)
_block($xbeg, $xlen, $ybeg, $ylen, $edits)
_changed($orig, $closing)
_lines($lines, $prefix=' ')
$leading_context_lines
Number of leading context "lines" to preserve.
format($diff)
Format a diff.
$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)
exit
Definition: login.php:28
$i
Definition: metadata.php:41
__construct(Container $dic, ilPlugin $plugin)
@inheritDoc
string $key
Consumer key/client ID value.
Definition: System.php:193
$context
Definition: webdav.php:29