Moved 'get previous/next revision' code from DifferenceEngine to Title'
[lhc/web/wiklou.git] / includes / DifferenceEngine.php
1 <?php
2 /**
3 * See diff.doc
4 * @package MediaWiki
5 */
6
7 /**
8 * @todo document
9 * @package MediaWiki
10 */
11 class DifferenceEngine {
12 /* private */ var $mOldid, $mNewid;
13 /* private */ var $mOldtitle, $mNewtitle, $mPagetitle;
14 /* private */ var $mOldtext, $mNewtext;
15 /* private */ var $mOldUser, $mNewUser;
16 /* private */ var $mOldComment, $mNewComment;
17 /* private */ var $mOldPage, $mNewPage;
18 /* private */ var $mRcidMarkPatrolled;
19
20 function DifferenceEngine( $old, $new, $rcid = 0 )
21 {
22 global $wgTitle;
23 if ( 'prev' == $new ) {
24 # Show diff between revision $old and the previous one.
25 # Get previous one from DB.
26 #
27 $this->mNewid = intval($old);
28
29 $this->mOldid = $wgTitle->getPreviousRevisionID( $this->mNewid );
30 #$dbr =& wfGetDB( DB_SLAVE );
31 #$this->mOldid = $dbr->selectField( 'old', 'old_id',
32 #"old_title='" . $wgTitle->getDBkey() . "'" .
33 #' AND old_namespace=' . $wgTitle->getNamespace() .
34 #" AND old_id<{$this->mNewid} ORDER BY old_id DESC" );
35
36 } elseif ( 'next' == $new ) {
37
38 # Show diff between revision $old and the previous one.
39 # Get previous one from DB.
40 #
41 $this->mOldid = intval($old);
42 $this->mNewid = $wgTitle->getNextRevisionID( $this->mOldid );
43 #$dbr =& wfGetDB( DB_SLAVE );
44 #$this->mNewid = $dbr->selectField( 'old', 'old_id',
45 # "old_title='" . $wgTitle->getDBkey() . "'" .
46 # ' AND old_namespace=' . $wgTitle->getNamespace() .
47 # " AND old_id>{$this->mOldid} ORDER BY old_id " );
48 if ( false === $this->mNewid ) {
49 # if no result, NewId points to the newest old revision. The only newer
50 # revision is cur, which is "0".
51 $this->mNewid = 0;
52 }
53
54 } else {
55
56 $this->mOldid = intval($old);
57 $this->mNewid = intval($new);
58 }
59 $this->mRcidMarkPatrolled = intval($rcid); # force it to be an integer
60 }
61
62 function showDiffPage()
63 {
64 global $wgUser, $wgTitle, $wgOut, $wgContLang, $wgOnlySysopsCanPatrol, $wgUseRCPatrol;
65 $fname = 'DifferenceEngine::showDiffPage';
66 wfProfileIn( $fname );
67
68 # mOldid is false if the difference engine is called with a "vague" query for
69 # a diff between a version V and its previous version V' AND the version V
70 # is the first version of that article. In that case, V' does not exist.
71 if ( $this->mOldid === false ) {
72 $this->showFirstRevision();
73 wfProfileOut( $fname );
74 return;
75 }
76
77 $t = $wgTitle->getPrefixedText() . " (Diff: {$this->mOldid}, " .
78 "{$this->mNewid})";
79 $mtext = wfMsg( 'missingarticle', $t );
80
81 $wgOut->setArticleFlag( false );
82 if ( ! $this->loadText() ) {
83 $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
84 $wgOut->addHTML( $mtext );
85 wfProfileOut( $fname );
86 return;
87 }
88 $wgOut->suppressQuickbar();
89
90 $oldTitle = $this->mOldPage->getPrefixedText();
91 $newTitle = $this->mNewPage->getPrefixedText();
92 if( $oldTitle == $newTitle ) {
93 $wgOut->setPageTitle( $newTitle );
94 } else {
95 $wgOut->setPageTitle( $oldTitle . ', ' . $newTitle );
96 }
97 $wgOut->setSubtitle( wfMsg( 'difference' ) );
98 $wgOut->setRobotpolicy( 'noindex,follow' );
99
100 if ( !( $this->mOldPage->userCanRead() && $this->mNewPage->userCanRead() ) ) {
101 $wgOut->loginToUse();
102 $wgOut->output();
103 wfProfileOut( $fname );
104 exit;
105 }
106
107 $sk = $wgUser->getSkin();
108 $talk = $wgContLang->getNsText( NS_TALK );
109 $contribs = wfMsg( 'contribslink' );
110
111 $this->mOldComment = $sk->formatComment($this->mOldComment);
112 $this->mNewComment = $sk->formatComment($this->mNewComment);
113
114 $oldUserLink = $sk->makeLinkObj( Title::makeTitleSafe( NS_USER, $this->mOldUser ), $this->mOldUser );
115 $newUserLink = $sk->makeLinkObj( Title::makeTitleSafe( NS_USER, $this->mNewUser ), $this->mNewUser );
116 $oldUTLink = $sk->makeLinkObj( Title::makeTitleSafe( NS_USER_TALK, $this->mOldUser ), $talk );
117 $newUTLink = $sk->makeLinkObj( Title::makeTitleSafe( NS_USER_TALK, $this->mNewUser ), $talk );
118 $oldContribs = $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Contributions' ), $contribs,
119 'target=' . urlencode($this->mOldUser) );
120 $newContribs = $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Contributions' ), $contribs,
121 'target=' . urlencode($this->mNewUser) );
122 if ( !$this->mNewid && $wgUser->isSysop() ) {
123 $rollback = '&nbsp;&nbsp;&nbsp;<strong>[' . $sk->makeKnownLinkObj( $wgTitle, wfMsg( 'rollbacklink' ),
124 'action=rollback&from=' . urlencode($this->mNewUser) ) . ']</strong>';
125 } else {
126 $rollback = '';
127 }
128 if ( $wgUseRCPatrol && $this->mRcidMarkPatrolled != 0 && $wgUser->getID() != 0 &&
129 ( $wgUser->isSysop() || !$wgOnlySysopsCanPatrol ) )
130 {
131 $patrol = ' [' . $sk->makeKnownLinkObj( $wgTitle, wfMsg( 'markaspatrolleddiff' ),
132 "action=markpatrolled&rcid={$this->mRcidMarkPatrolled}" ) . ']';
133 } else {
134 $patrol = '';
135 }
136
137 $prevlink = $sk->makeKnownLinkObj( $wgTitle, wfMsg( 'previousdiff' ), 'diff=prev&oldid='.$this->mOldid );
138 if ( $this->mNewid == 0 ) {
139 $nextlink = '';
140 } else {
141 $nextlink = $sk->makeKnownLinkObj( $wgTitle, wfMsg( 'nextdiff' ), 'diff=next&oldid='.$this->mNewid );
142 }
143
144 $oldHeader = "<strong>{$this->mOldtitle}</strong><br />$oldUserLink " .
145 "($oldUTLink | $oldContribs)<br />" . $this->mOldComment .
146 '<br />' . $prevlink;
147 $newHeader = "<strong>{$this->mNewtitle}</strong><br />$newUserLink " .
148 "($newUTLink | $newContribs) $rollback<br />" . $this->mNewComment .
149 '<br />' . $nextlink . $patrol;
150
151 DifferenceEngine::showDiff( $this->mOldtext, $this->mNewtext,
152 $oldHeader, $newHeader );
153 $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" );
154 $wgOut->addWikiText( $this->mNewtext );
155
156 wfProfileOut( $fname );
157 }
158
159 # Show the first revision of an article. Uses normal diff headers in contrast to normal
160 # "old revision" display style.
161 #
162 function showFirstRevision()
163 {
164 global $wgOut, $wgTitle, $wgUser, $wgLang;
165
166 $fname = 'DifferenceEngine::showFirstRevision';
167 wfProfileIn( $fname );
168
169
170 $this->mOldid = $this->mNewid; # hack to make loadText() work.
171
172 # Get article text from the DB
173 #
174 if ( ! $this->loadText() ) {
175 $t = $wgTitle->getPrefixedText() . " (Diff: {$this->mOldid}, " .
176 "{$this->mNewid})";
177 $mtext = wfMsg( 'missingarticle', $t );
178 $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
179 $wgOut->addHTML( $mtext );
180 wfProfileOut( $fname );
181 return;
182 }
183
184 # Check if user is allowed to look at this page. If not, bail out.
185 #
186 if ( !( $this->mOldPage->userCanRead() ) ) {
187 $wgOut->loginToUse();
188 $wgOut->output();
189 wfProfileOut( $fname );
190 exit;
191 }
192
193 # Prepare the header box
194 #
195 $sk = $wgUser->getSkin();
196
197 $uTLink = $sk->makeLinkObj( Title::makeTitleSafe( NS_USER_TALK, $this->mOldUser ), $wgLang->getNsText( NS_TALK ) );
198 $userLink = $sk->makeLinkObj( Title::makeTitleSafe( NS_USER, $this->mOldUser ), $this->mOldUser );
199 $contribs = $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Contributions' ), wfMsg( 'contribslink' ),
200 'target=' . urlencode($this->mOldUser) );
201 $nextlink = $sk->makeKnownLinkObj( $wgTitle, wfMsg( 'nextdiff' ), 'diff=next&oldid='.$this->mNewid );
202 $header = "<div class=\"firstrevisionheader\" style=\"text-align: center\"><strong>{$this->mOldtitle}</strong><br />$userLink " .
203 "($uTLink | $contribs)<br />" . $this->mOldComment .
204 '<br />' . $nextlink. "</div>\n";
205
206 $wgOut->addHTML( $header );
207
208 $wgOut->setSubtitle( wfMsg( 'difference' ) );
209 $wgOut->setRobotpolicy( 'noindex,follow' );
210
211
212 # Show current revision
213 #
214 $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" );
215 $wgOut->addWikiText( $this->mNewtext );
216
217 wfProfileOut( $fname );
218 }
219
220 function showDiff( $otext, $ntext, $otitle, $ntitle )
221 {
222 global $wgOut, $wgUseExternalDiffEngine;
223
224 $wgOut->addHTML( "
225 <table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'>
226 <tr>
227 <td colspan='2' width='50%' align='center' class='diff-otitle'>{$otitle}</td>
228 <td colspan='2' width='50%' align='center' class='diff-ntitle'>{$ntitle}</td>
229 </tr>
230 " );
231
232 if ( $wgUseExternalDiffEngine ) {
233 # For historical reasons, external diff engine expects
234 # input text to be HTML-escaped already
235 $otext = str_replace( "\r\n", "\n", htmlspecialchars ( $otext ) );
236 $ntext = str_replace( "\r\n", "\n", htmlspecialchars ( $ntext ) );
237 dl('php_wikidiff.so');
238 $wgOut->addHTML( wikidiff_do_diff( $otext, $ntext, 2) );
239 } else {
240 $ota = explode( "\n", str_replace( "\r\n", "\n", $otext ) );
241 $nta = explode( "\n", str_replace( "\r\n", "\n", $ntext ) );
242 $diffs = new Diff( $ota, $nta );
243 $formatter = new TableDiffFormatter();
244 $formatter->format( $diffs );
245 }
246 $wgOut->addHTML( "</table>\n" );
247 }
248
249 # Load the text of the articles to compare. If newid is 0, then compare
250 # the old article in oldid to the current article; if oldid is 0, then
251 # compare the current article to the immediately previous one (ignoring
252 # the value of newid).
253 #
254 function loadText()
255 {
256 global $wgTitle, $wgOut, $wgLang;
257 $fname = 'DifferenceEngine::loadText';
258
259 $dbr =& wfGetDB( DB_SLAVE );
260 if ( 0 == $this->mNewid || 0 == $this->mOldid ) {
261 $wgOut->setArticleFlag( true );
262 $newLink = $wgTitle->getLocalUrl();
263 $this->mPagetitle = wfMsg( 'currentrev' );
264 $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
265 $id = $wgTitle->getArticleID();
266
267 $s = $dbr->getArray( 'cur', array( 'cur_text', 'cur_user_text', 'cur_comment' ),
268 array( 'cur_id' => $id ), $fname );
269 if ( $s === false ) {
270 wfDebug( "Unable to load cur_id $id\n" );
271 return false;
272 }
273
274 $this->mNewPage = &$wgTitle;
275 $this->mNewtext = $s->cur_text;
276 $this->mNewUser = $s->cur_user_text;
277 $this->mNewComment = $s->cur_comment;
278 } else {
279 $s = $dbr->getArray( 'old', array( 'old_namespace','old_title','old_timestamp', 'old_text',
280 'old_flags','old_user_text','old_comment' ), array( 'old_id' => $this->mNewid ), $fname );
281
282 if ( $s === false ) {
283 wfDebug( "Unable to load old_id {$this->mNewid}\n" );
284 return false;
285 }
286
287 $this->mNewtext = Article::getRevisionText( $s );
288
289 $t = $wgLang->timeanddate( $s->old_timestamp, true );
290 $this->mNewPage = Title::MakeTitle( $s->old_namespace, $s->old_title );
291 $newLink = $wgTitle->getLocalUrl ('oldid=' . $this->mNewid);
292 $this->mPagetitle = wfMsg( 'revisionasof', $t );
293 $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
294 $this->mNewUser = $s->old_user_text;
295 $this->mNewComment = $s->old_comment;
296 }
297 if ( 0 == $this->mOldid ) {
298 $s = $dbr->getArray( 'old',
299 array( 'old_namespace','old_title','old_timestamp','old_text', 'old_flags','old_user_text','old_comment' ),
300 array( /* WHERE */
301 'old_namespace' => $this->mNewPage->getNamespace(),
302 'old_title' => $this->mNewPage->getDBkey()
303 ), $fname, array( 'ORDER BY' => 'inverse_timestamp', 'USE INDEX' => 'name_title_timestamp' )
304 );
305 if ( $s === false ) {
306 wfDebug( 'Unable to load ' . $this->mNewPage->getPrefixedDBkey . " from old\n" );
307 return false;
308 }
309 } else {
310 $s = $dbr->getArray( 'old',
311 array( 'old_namespace','old_title','old_timestamp','old_text','old_flags','old_user_text','old_comment'),
312 array( 'old_id' => $this->mOldid ),
313 $fname
314 );
315 if ( $s === false ) {
316 wfDebug( "Unable to load old_id {$this->mOldid}\n" );
317 return false;
318 }
319 }
320 $this->mOldPage = Title::MakeTitle( $s->old_namespace, $s->old_title );
321 $this->mOldtext = Article::getRevisionText( $s );
322
323 $t = $wgLang->timeanddate( $s->old_timestamp, true );
324 $oldLink = $this->mOldPage->getLocalUrl ('oldid=' . $this->mOldid);
325 $this->mOldtitle = "<a href='$oldLink'>" . wfMsg( 'revisionasof', $t ) . '</a>';
326 $this->mOldUser = $s->old_user_text;
327 $this->mOldComment = $s->old_comment;
328
329 return true;
330 }
331 }
332
333 // A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
334 //
335 // Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
336 // You may copy this code freely under the conditions of the GPL.
337 //
338
339 define('USE_ASSERTS', function_exists('assert'));
340
341 /**
342 * @todo document
343 * @package MediaWiki
344 */
345 class _DiffOp {
346 var $type;
347 var $orig;
348 var $closing;
349
350 function reverse() {
351 trigger_error('pure virtual', E_USER_ERROR);
352 }
353
354 function norig() {
355 return $this->orig ? sizeof($this->orig) : 0;
356 }
357
358 function nclosing() {
359 return $this->closing ? sizeof($this->closing) : 0;
360 }
361 }
362
363 /**
364 * @todo document
365 * @package MediaWiki
366 */
367 class _DiffOp_Copy extends _DiffOp {
368 var $type = 'copy';
369
370 function _DiffOp_Copy ($orig, $closing = false) {
371 if (!is_array($closing))
372 $closing = $orig;
373 $this->orig = $orig;
374 $this->closing = $closing;
375 }
376
377 function reverse() {
378 return new _DiffOp_Copy($this->closing, $this->orig);
379 }
380 }
381
382 /**
383 * @todo document
384 * @package MediaWiki
385 */
386 class _DiffOp_Delete extends _DiffOp {
387 var $type = 'delete';
388
389 function _DiffOp_Delete ($lines) {
390 $this->orig = $lines;
391 $this->closing = false;
392 }
393
394 function reverse() {
395 return new _DiffOp_Add($this->orig);
396 }
397 }
398
399 /**
400 * @todo document
401 * @package MediaWiki
402 */
403 class _DiffOp_Add extends _DiffOp {
404 var $type = 'add';
405
406 function _DiffOp_Add ($lines) {
407 $this->closing = $lines;
408 $this->orig = false;
409 }
410
411 function reverse() {
412 return new _DiffOp_Delete($this->closing);
413 }
414 }
415
416 /**
417 * @todo document
418 * @package MediaWiki
419 */
420 class _DiffOp_Change extends _DiffOp {
421 var $type = 'change';
422
423 function _DiffOp_Change ($orig, $closing) {
424 $this->orig = $orig;
425 $this->closing = $closing;
426 }
427
428 function reverse() {
429 return new _DiffOp_Change($this->closing, $this->orig);
430 }
431 }
432
433
434 /**
435 * Class used internally by Diff to actually compute the diffs.
436 *
437 * The algorithm used here is mostly lifted from the perl module
438 * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
439 * http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
440 *
441 * More ideas are taken from:
442 * http://www.ics.uci.edu/~eppstein/161/960229.html
443 *
444 * Some ideas are (and a bit of code) are from from analyze.c, from GNU
445 * diffutils-2.7, which can be found at:
446 * ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
447 *
448 * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
449 * are my own.
450 *
451 * @author Geoffrey T. Dairiki
452 * @package MediaWiki
453 * @access private
454 */
455 class _DiffEngine
456 {
457 function diff ($from_lines, $to_lines) {
458 $n_from = sizeof($from_lines);
459 $n_to = sizeof($to_lines);
460
461 $this->xchanged = $this->ychanged = array();
462 $this->xv = $this->yv = array();
463 $this->xind = $this->yind = array();
464 unset($this->seq);
465 unset($this->in_seq);
466 unset($this->lcs);
467
468 // Skip leading common lines.
469 for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
470 if ($from_lines[$skip] != $to_lines[$skip])
471 break;
472 $this->xchanged[$skip] = $this->ychanged[$skip] = false;
473 }
474 // Skip trailing common lines.
475 $xi = $n_from; $yi = $n_to;
476 for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
477 if ($from_lines[$xi] != $to_lines[$yi])
478 break;
479 $this->xchanged[$xi] = $this->ychanged[$yi] = false;
480 }
481
482 // Ignore lines which do not exist in both files.
483 for ($xi = $skip; $xi < $n_from - $endskip; $xi++)
484 $xhash[$from_lines[$xi]] = 1;
485 for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
486 $line = $to_lines[$yi];
487 if ( ($this->ychanged[$yi] = empty($xhash[$line])) )
488 continue;
489 $yhash[$line] = 1;
490 $this->yv[] = $line;
491 $this->yind[] = $yi;
492 }
493 for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
494 $line = $from_lines[$xi];
495 if ( ($this->xchanged[$xi] = empty($yhash[$line])) )
496 continue;
497 $this->xv[] = $line;
498 $this->xind[] = $xi;
499 }
500
501 // Find the LCS.
502 $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
503
504 // Merge edits when possible
505 $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
506 $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
507
508 // Compute the edit operations.
509 $edits = array();
510 $xi = $yi = 0;
511 while ($xi < $n_from || $yi < $n_to) {
512 USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
513 USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
514
515 // Skip matching "snake".
516 $copy = array();
517 while ( $xi < $n_from && $yi < $n_to
518 && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
519 $copy[] = $from_lines[$xi++];
520 ++$yi;
521 }
522 if ($copy)
523 $edits[] = new _DiffOp_Copy($copy);
524
525 // Find deletes & adds.
526 $delete = array();
527 while ($xi < $n_from && $this->xchanged[$xi])
528 $delete[] = $from_lines[$xi++];
529
530 $add = array();
531 while ($yi < $n_to && $this->ychanged[$yi])
532 $add[] = $to_lines[$yi++];
533
534 if ($delete && $add)
535 $edits[] = new _DiffOp_Change($delete, $add);
536 elseif ($delete)
537 $edits[] = new _DiffOp_Delete($delete);
538 elseif ($add)
539 $edits[] = new _DiffOp_Add($add);
540 }
541 return $edits;
542 }
543
544
545 /* Divide the Largest Common Subsequence (LCS) of the sequences
546 * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
547 * sized segments.
548 *
549 * Returns (LCS, PTS). LCS is the length of the LCS. PTS is an
550 * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
551 * sub sequences. The first sub-sequence is contained in [X0, X1),
552 * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on. Note
553 * that (X0, Y0) == (XOFF, YOFF) and
554 * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
555 *
556 * This function assumes that the first lines of the specified portions
557 * of the two files do not match, and likewise that the last lines do not
558 * match. The caller must trim matching lines from the beginning and end
559 * of the portions it is going to specify.
560 */
561 function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks) {
562 $flip = false;
563
564 if ($xlim - $xoff > $ylim - $yoff) {
565 // Things seems faster (I'm not sure I understand why)
566 // when the shortest sequence in X.
567 $flip = true;
568 list ($xoff, $xlim, $yoff, $ylim)
569 = array( $yoff, $ylim, $xoff, $xlim);
570 }
571
572 if ($flip)
573 for ($i = $ylim - 1; $i >= $yoff; $i--)
574 $ymatches[$this->xv[$i]][] = $i;
575 else
576 for ($i = $ylim - 1; $i >= $yoff; $i--)
577 $ymatches[$this->yv[$i]][] = $i;
578
579 $this->lcs = 0;
580 $this->seq[0]= $yoff - 1;
581 $this->in_seq = array();
582 $ymids[0] = array();
583
584 $numer = $xlim - $xoff + $nchunks - 1;
585 $x = $xoff;
586 for ($chunk = 0; $chunk < $nchunks; $chunk++) {
587 if ($chunk > 0)
588 for ($i = 0; $i <= $this->lcs; $i++)
589 $ymids[$i][$chunk-1] = $this->seq[$i];
590
591 $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
592 for ( ; $x < $x1; $x++) {
593 $line = $flip ? $this->yv[$x] : $this->xv[$x];
594 if (empty($ymatches[$line]))
595 continue;
596 $matches = $ymatches[$line];
597 reset($matches);
598 while (list ($junk, $y) = each($matches))
599 if (empty($this->in_seq[$y])) {
600 $k = $this->_lcs_pos($y);
601 USE_ASSERTS && assert($k > 0);
602 $ymids[$k] = $ymids[$k-1];
603 break;
604 }
605 while (list ($junk, $y) = each($matches)) {
606 if ($y > $this->seq[$k-1]) {
607 USE_ASSERTS && assert($y < $this->seq[$k]);
608 // Optimization: this is a common case:
609 // next match is just replacing previous match.
610 $this->in_seq[$this->seq[$k]] = false;
611 $this->seq[$k] = $y;
612 $this->in_seq[$y] = 1;
613 }
614 else if (empty($this->in_seq[$y])) {
615 $k = $this->_lcs_pos($y);
616 USE_ASSERTS && assert($k > 0);
617 $ymids[$k] = $ymids[$k-1];
618 }
619 }
620 }
621 }
622
623 $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
624 $ymid = $ymids[$this->lcs];
625 for ($n = 0; $n < $nchunks - 1; $n++) {
626 $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
627 $y1 = $ymid[$n] + 1;
628 $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
629 }
630 $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
631
632 return array($this->lcs, $seps);
633 }
634
635 function _lcs_pos ($ypos) {
636 $end = $this->lcs;
637 if ($end == 0 || $ypos > $this->seq[$end]) {
638 $this->seq[++$this->lcs] = $ypos;
639 $this->in_seq[$ypos] = 1;
640 return $this->lcs;
641 }
642
643 $beg = 1;
644 while ($beg < $end) {
645 $mid = (int)(($beg + $end) / 2);
646 if ( $ypos > $this->seq[$mid] )
647 $beg = $mid + 1;
648 else
649 $end = $mid;
650 }
651
652 USE_ASSERTS && assert($ypos != $this->seq[$end]);
653
654 $this->in_seq[$this->seq[$end]] = false;
655 $this->seq[$end] = $ypos;
656 $this->in_seq[$ypos] = 1;
657 return $end;
658 }
659
660 /* Find LCS of two sequences.
661 *
662 * The results are recorded in the vectors $this->{x,y}changed[], by
663 * storing a 1 in the element for each line that is an insertion
664 * or deletion (ie. is not in the LCS).
665 *
666 * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
667 *
668 * Note that XLIM, YLIM are exclusive bounds.
669 * All line numbers are origin-0 and discarded lines are not counted.
670 */
671 function _compareseq ($xoff, $xlim, $yoff, $ylim) {
672 // Slide down the bottom initial diagonal.
673 while ($xoff < $xlim && $yoff < $ylim
674 && $this->xv[$xoff] == $this->yv[$yoff]) {
675 ++$xoff;
676 ++$yoff;
677 }
678
679 // Slide up the top initial diagonal.
680 while ($xlim > $xoff && $ylim > $yoff
681 && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
682 --$xlim;
683 --$ylim;
684 }
685
686 if ($xoff == $xlim || $yoff == $ylim)
687 $lcs = 0;
688 else {
689 // This is ad hoc but seems to work well.
690 //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
691 //$nchunks = max(2,min(8,(int)$nchunks));
692 $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
693 list ($lcs, $seps)
694 = $this->_diag($xoff,$xlim,$yoff, $ylim,$nchunks);
695 }
696
697 if ($lcs == 0) {
698 // X and Y sequences have no common subsequence:
699 // mark all changed.
700 while ($yoff < $ylim)
701 $this->ychanged[$this->yind[$yoff++]] = 1;
702 while ($xoff < $xlim)
703 $this->xchanged[$this->xind[$xoff++]] = 1;
704 }
705 else {
706 // Use the partitions to split this problem into subproblems.
707 reset($seps);
708 $pt1 = $seps[0];
709 while ($pt2 = next($seps)) {
710 $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
711 $pt1 = $pt2;
712 }
713 }
714 }
715
716 /* Adjust inserts/deletes of identical lines to join changes
717 * as much as possible.
718 *
719 * We do something when a run of changed lines include a
720 * line at one end and has an excluded, identical line at the other.
721 * We are free to choose which identical line is included.
722 * `compareseq' usually chooses the one at the beginning,
723 * but usually it is cleaner to consider the following identical line
724 * to be the "change".
725 *
726 * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
727 */
728 function _shift_boundaries ($lines, &$changed, $other_changed) {
729 $i = 0;
730 $j = 0;
731
732 USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
733 $len = sizeof($lines);
734 $other_len = sizeof($other_changed);
735
736 while (1) {
737 /*
738 * Scan forwards to find beginning of another run of changes.
739 * Also keep track of the corresponding point in the other file.
740 *
741 * Throughout this code, $i and $j are adjusted together so that
742 * the first $i elements of $changed and the first $j elements
743 * of $other_changed both contain the same number of zeros
744 * (unchanged lines).
745 * Furthermore, $j is always kept so that $j == $other_len or
746 * $other_changed[$j] == false.
747 */
748 while ($j < $other_len && $other_changed[$j])
749 $j++;
750
751 while ($i < $len && ! $changed[$i]) {
752 USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
753 $i++; $j++;
754 while ($j < $other_len && $other_changed[$j])
755 $j++;
756 }
757
758 if ($i == $len)
759 break;
760
761 $start = $i;
762
763 // Find the end of this run of changes.
764 while (++$i < $len && $changed[$i])
765 continue;
766
767 do {
768 /*
769 * Record the length of this run of changes, so that
770 * we can later determine whether the run has grown.
771 */
772 $runlength = $i - $start;
773
774 /*
775 * Move the changed region back, so long as the
776 * previous unchanged line matches the last changed one.
777 * This merges with previous changed regions.
778 */
779 while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
780 $changed[--$start] = 1;
781 $changed[--$i] = false;
782 while ($start > 0 && $changed[$start - 1])
783 $start--;
784 USE_ASSERTS && assert('$j > 0');
785 while ($other_changed[--$j])
786 continue;
787 USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
788 }
789
790 /*
791 * Set CORRESPONDING to the end of the changed run, at the last
792 * point where it corresponds to a changed run in the other file.
793 * CORRESPONDING == LEN means no such point has been found.
794 */
795 $corresponding = $j < $other_len ? $i : $len;
796
797 /*
798 * Move the changed region forward, so long as the
799 * first changed line matches the following unchanged one.
800 * This merges with following changed regions.
801 * Do this second, so that if there are no merges,
802 * the changed region is moved forward as far as possible.
803 */
804 while ($i < $len && $lines[$start] == $lines[$i]) {
805 $changed[$start++] = false;
806 $changed[$i++] = 1;
807 while ($i < $len && $changed[$i])
808 $i++;
809
810 USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
811 $j++;
812 if ($j < $other_len && $other_changed[$j]) {
813 $corresponding = $i;
814 while ($j < $other_len && $other_changed[$j])
815 $j++;
816 }
817 }
818 } while ($runlength != $i - $start);
819
820 /*
821 * If possible, move the fully-merged run of changes
822 * back to a corresponding run in the other file.
823 */
824 while ($corresponding < $i) {
825 $changed[--$start] = 1;
826 $changed[--$i] = 0;
827 USE_ASSERTS && assert('$j > 0');
828 while ($other_changed[--$j])
829 continue;
830 USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
831 }
832 }
833 }
834 }
835
836 /**
837 * Class representing a 'diff' between two sequences of strings.
838 * @todo document
839 * @package MediaWiki
840 */
841 class Diff
842 {
843 var $edits;
844
845 /**
846 * Constructor.
847 * Computes diff between sequences of strings.
848 *
849 * @param $from_lines array An array of strings.
850 * (Typically these are lines from a file.)
851 * @param $to_lines array An array of strings.
852 */
853 function Diff($from_lines, $to_lines) {
854 $eng = new _DiffEngine;
855 $this->edits = $eng->diff($from_lines, $to_lines);
856 //$this->_check($from_lines, $to_lines);
857 }
858
859 /**
860 * Compute reversed Diff.
861 *
862 * SYNOPSIS:
863 *
864 * $diff = new Diff($lines1, $lines2);
865 * $rev = $diff->reverse();
866 * @return object A Diff object representing the inverse of the
867 * original diff.
868 */
869 function reverse () {
870 $rev = $this;
871 $rev->edits = array();
872 foreach ($this->edits as $edit) {
873 $rev->edits[] = $edit->reverse();
874 }
875 return $rev;
876 }
877
878 /**
879 * Check for empty diff.
880 *
881 * @return bool True iff two sequences were identical.
882 */
883 function isEmpty () {
884 foreach ($this->edits as $edit) {
885 if ($edit->type != 'copy')
886 return false;
887 }
888 return true;
889 }
890
891 /**
892 * Compute the length of the Longest Common Subsequence (LCS).
893 *
894 * This is mostly for diagnostic purposed.
895 *
896 * @return int The length of the LCS.
897 */
898 function lcs () {
899 $lcs = 0;
900 foreach ($this->edits as $edit) {
901 if ($edit->type == 'copy')
902 $lcs += sizeof($edit->orig);
903 }
904 return $lcs;
905 }
906
907 /**
908 * Get the original set of lines.
909 *
910 * This reconstructs the $from_lines parameter passed to the
911 * constructor.
912 *
913 * @return array The original sequence of strings.
914 */
915 function orig() {
916 $lines = array();
917
918 foreach ($this->edits as $edit) {
919 if ($edit->orig)
920 array_splice($lines, sizeof($lines), 0, $edit->orig);
921 }
922 return $lines;
923 }
924
925 /**
926 * Get the closing set of lines.
927 *
928 * This reconstructs the $to_lines parameter passed to the
929 * constructor.
930 *
931 * @return array The sequence of strings.
932 */
933 function closing() {
934 $lines = array();
935
936 foreach ($this->edits as $edit) {
937 if ($edit->closing)
938 array_splice($lines, sizeof($lines), 0, $edit->closing);
939 }
940 return $lines;
941 }
942
943 /**
944 * Check a Diff for validity.
945 *
946 * This is here only for debugging purposes.
947 */
948 function _check ($from_lines, $to_lines) {
949 if (serialize($from_lines) != serialize($this->orig()))
950 trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
951 if (serialize($to_lines) != serialize($this->closing()))
952 trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
953
954 $rev = $this->reverse();
955 if (serialize($to_lines) != serialize($rev->orig()))
956 trigger_error("Reversed original doesn't match", E_USER_ERROR);
957 if (serialize($from_lines) != serialize($rev->closing()))
958 trigger_error("Reversed closing doesn't match", E_USER_ERROR);
959
960
961 $prevtype = 'none';
962 foreach ($this->edits as $edit) {
963 if ( $prevtype == $edit->type )
964 trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
965 $prevtype = $edit->type;
966 }
967
968 $lcs = $this->lcs();
969 trigger_error('Diff okay: LCS = '.$lcs, E_USER_NOTICE);
970 }
971 }
972
973 /**
974 * FIXME: bad name.
975 * @todo document
976 * @package MediaWiki
977 */
978 class MappedDiff extends Diff
979 {
980 /**
981 * Constructor.
982 *
983 * Computes diff between sequences of strings.
984 *
985 * This can be used to compute things like
986 * case-insensitve diffs, or diffs which ignore
987 * changes in white-space.
988 *
989 * @param $from_lines array An array of strings.
990 * (Typically these are lines from a file.)
991 *
992 * @param $to_lines array An array of strings.
993 *
994 * @param $mapped_from_lines array This array should
995 * have the same size number of elements as $from_lines.
996 * The elements in $mapped_from_lines and
997 * $mapped_to_lines are what is actually compared
998 * when computing the diff.
999 *
1000 * @param $mapped_to_lines array This array should
1001 * have the same number of elements as $to_lines.
1002 */
1003 function MappedDiff($from_lines, $to_lines,
1004 $mapped_from_lines, $mapped_to_lines) {
1005
1006 assert(sizeof($from_lines) == sizeof($mapped_from_lines));
1007 assert(sizeof($to_lines) == sizeof($mapped_to_lines));
1008
1009 $this->Diff($mapped_from_lines, $mapped_to_lines);
1010
1011 $xi = $yi = 0;
1012 for ($i = 0; $i < sizeof($this->edits); $i++) {
1013 $orig = &$this->edits[$i]->orig;
1014 if (is_array($orig)) {
1015 $orig = array_slice($from_lines, $xi, sizeof($orig));
1016 $xi += sizeof($orig);
1017 }
1018
1019 $closing = &$this->edits[$i]->closing;
1020 if (is_array($closing)) {
1021 $closing = array_slice($to_lines, $yi, sizeof($closing));
1022 $yi += sizeof($closing);
1023 }
1024 }
1025 }
1026 }
1027
1028 /**
1029 * A class to format Diffs
1030 *
1031 * This class formats the diff in classic diff format.
1032 * It is intended that this class be customized via inheritance,
1033 * to obtain fancier outputs.
1034 * @todo document
1035 * @package MediaWiki
1036 */
1037 class DiffFormatter
1038 {
1039 /**
1040 * Number of leading context "lines" to preserve.
1041 *
1042 * This should be left at zero for this class, but subclasses
1043 * may want to set this to other values.
1044 */
1045 var $leading_context_lines = 0;
1046
1047 /**
1048 * Number of trailing context "lines" to preserve.
1049 *
1050 * This should be left at zero for this class, but subclasses
1051 * may want to set this to other values.
1052 */
1053 var $trailing_context_lines = 0;
1054
1055 /**
1056 * Format a diff.
1057 *
1058 * @param $diff object A Diff object.
1059 * @return string The formatted output.
1060 */
1061 function format($diff) {
1062
1063 $xi = $yi = 1;
1064 $block = false;
1065 $context = array();
1066
1067 $nlead = $this->leading_context_lines;
1068 $ntrail = $this->trailing_context_lines;
1069
1070 $this->_start_diff();
1071
1072 foreach ($diff->edits as $edit) {
1073 if ($edit->type == 'copy') {
1074 if (is_array($block)) {
1075 if (sizeof($edit->orig) <= $nlead + $ntrail) {
1076 $block[] = $edit;
1077 }
1078 else{
1079 if ($ntrail) {
1080 $context = array_slice($edit->orig, 0, $ntrail);
1081 $block[] = new _DiffOp_Copy($context);
1082 }
1083 $this->_block($x0, $ntrail + $xi - $x0,
1084 $y0, $ntrail + $yi - $y0,
1085 $block);
1086 $block = false;
1087 }
1088 }
1089 $context = $edit->orig;
1090 }
1091 else {
1092 if (! is_array($block)) {
1093 $context = array_slice($context, sizeof($context) - $nlead);
1094 $x0 = $xi - sizeof($context);
1095 $y0 = $yi - sizeof($context);
1096 $block = array();
1097 if ($context)
1098 $block[] = new _DiffOp_Copy($context);
1099 }
1100 $block[] = $edit;
1101 }
1102
1103 if ($edit->orig)
1104 $xi += sizeof($edit->orig);
1105 if ($edit->closing)
1106 $yi += sizeof($edit->closing);
1107 }
1108
1109 if (is_array($block))
1110 $this->_block($x0, $xi - $x0,
1111 $y0, $yi - $y0,
1112 $block);
1113
1114 return $this->_end_diff();
1115 }
1116
1117 function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
1118 $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
1119 foreach ($edits as $edit) {
1120 if ($edit->type == 'copy')
1121 $this->_context($edit->orig);
1122 elseif ($edit->type == 'add')
1123 $this->_added($edit->closing);
1124 elseif ($edit->type == 'delete')
1125 $this->_deleted($edit->orig);
1126 elseif ($edit->type == 'change')
1127 $this->_changed($edit->orig, $edit->closing);
1128 else
1129 trigger_error('Unknown edit type', E_USER_ERROR);
1130 }
1131 $this->_end_block();
1132 }
1133
1134 function _start_diff() {
1135 ob_start();
1136 }
1137
1138 function _end_diff() {
1139 $val = ob_get_contents();
1140 ob_end_clean();
1141 return $val;
1142 }
1143
1144 function _block_header($xbeg, $xlen, $ybeg, $ylen) {
1145 if ($xlen > 1)
1146 $xbeg .= "," . ($xbeg + $xlen - 1);
1147 if ($ylen > 1)
1148 $ybeg .= "," . ($ybeg + $ylen - 1);
1149
1150 return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
1151 }
1152
1153 function _start_block($header) {
1154 echo $header;
1155 }
1156
1157 function _end_block() {
1158 }
1159
1160 function _lines($lines, $prefix = ' ') {
1161 foreach ($lines as $line)
1162 echo "$prefix $line\n";
1163 }
1164
1165 function _context($lines) {
1166 $this->_lines($lines);
1167 }
1168
1169 function _added($lines) {
1170 $this->_lines($lines, '>');
1171 }
1172 function _deleted($lines) {
1173 $this->_lines($lines, '<');
1174 }
1175
1176 function _changed($orig, $closing) {
1177 $this->_deleted($orig);
1178 echo "---\n";
1179 $this->_added($closing);
1180 }
1181 }
1182
1183
1184 /**
1185 * Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
1186 *
1187 */
1188
1189 define('NBSP', '&#160;'); // iso-8859-x non-breaking space.
1190
1191 /**
1192 * @todo document
1193 * @package MediaWiki
1194 */
1195 class _HWLDF_WordAccumulator {
1196 function _HWLDF_WordAccumulator () {
1197 $this->_lines = array();
1198 $this->_line = '';
1199 $this->_group = '';
1200 $this->_tag = '';
1201 }
1202
1203 function _flushGroup ($new_tag) {
1204 if ($this->_group !== '') {
1205 if ($this->_tag == 'mark')
1206 $this->_line .= '<span class="diffchange">' .
1207 htmlspecialchars ( $this->_group ) . '</span>';
1208 else
1209 $this->_line .= htmlspecialchars ( $this->_group );
1210 }
1211 $this->_group = '';
1212 $this->_tag = $new_tag;
1213 }
1214
1215 function _flushLine ($new_tag) {
1216 $this->_flushGroup($new_tag);
1217 if ($this->_line != '')
1218 array_push ( $this->_lines, $this->_line );
1219 else
1220 # make empty lines visible by inserting an NBSP
1221 array_push ( $this->_lines, NBSP );
1222 $this->_line = '';
1223 }
1224
1225 function addWords ($words, $tag = '') {
1226 if ($tag != $this->_tag)
1227 $this->_flushGroup($tag);
1228
1229 foreach ($words as $word) {
1230 // new-line should only come as first char of word.
1231 if ($word == '')
1232 continue;
1233 if ($word[0] == "\n") {
1234 $this->_flushLine($tag);
1235 $word = substr($word, 1);
1236 }
1237 assert(!strstr($word, "\n"));
1238 $this->_group .= $word;
1239 }
1240 }
1241
1242 function getLines() {
1243 $this->_flushLine('~done');
1244 return $this->_lines;
1245 }
1246 }
1247
1248 /**
1249 * @todo document
1250 * @package MediaWiki
1251 */
1252 class WordLevelDiff extends MappedDiff
1253 {
1254 function WordLevelDiff ($orig_lines, $closing_lines) {
1255 list ($orig_words, $orig_stripped) = $this->_split($orig_lines);
1256 list ($closing_words, $closing_stripped) = $this->_split($closing_lines);
1257
1258
1259 $this->MappedDiff($orig_words, $closing_words,
1260 $orig_stripped, $closing_stripped);
1261 }
1262
1263 function _split($lines) {
1264 if (!preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs',
1265 implode("\n", $lines),
1266 $m)) {
1267 return array(array(''), array(''));
1268 }
1269 return array($m[0], $m[1]);
1270 }
1271
1272 function orig () {
1273 $orig = new _HWLDF_WordAccumulator;
1274
1275 foreach ($this->edits as $edit) {
1276 if ($edit->type == 'copy')
1277 $orig->addWords($edit->orig);
1278 elseif ($edit->orig)
1279 $orig->addWords($edit->orig, 'mark');
1280 }
1281 return $orig->getLines();
1282 }
1283
1284 function closing () {
1285 $closing = new _HWLDF_WordAccumulator;
1286
1287 foreach ($this->edits as $edit) {
1288 if ($edit->type == 'copy')
1289 $closing->addWords($edit->closing);
1290 elseif ($edit->closing)
1291 $closing->addWords($edit->closing, 'mark');
1292 }
1293 return $closing->getLines();
1294 }
1295 }
1296
1297 /**
1298 * Wikipedia Table style diff formatter.
1299 * @todo document
1300 * @package MediaWiki
1301 */
1302 class TableDiffFormatter extends DiffFormatter
1303 {
1304 function TableDiffFormatter() {
1305 $this->leading_context_lines = 2;
1306 $this->trailing_context_lines = 2;
1307 }
1308
1309 function _block_header( $xbeg, $xlen, $ybeg, $ylen ) {
1310 $l1 = wfMsg( 'lineno', $xbeg );
1311 $l2 = wfMsg( 'lineno', $ybeg );
1312
1313 $r = '<tr><td colspan="2" align="left"><strong>'.$l1."</strong></td>\n" .
1314 '<td colspan="2" align="left"><strong>'.$l2."</strong></td></tr>\n";
1315 return $r;
1316 }
1317
1318 function _start_block( $header ) {
1319 global $wgOut;
1320 $wgOut->addHTML( $header );
1321 }
1322
1323 function _end_block() {
1324 }
1325
1326 function _lines( $lines, $prefix=' ', $color='white' ) {
1327 }
1328
1329 # HTML-escape parameter before calling this
1330 function addedLine( $line ) {
1331 return "<td>+</td><td class='diff-addedline'>{$line}</td>";
1332 }
1333
1334 # HTML-escape parameter before calling this
1335 function deletedLine( $line ) {
1336 return "<td>-</td><td class='diff-deletedline'>{$line}</td>";
1337 }
1338
1339 # HTML-escape parameter before calling this
1340 function contextLine( $line ) {
1341 return "<td> </td><td class='diff-context'>{$line}</td>";
1342 }
1343
1344 function emptyLine() {
1345 return '<td colspan="2">&nbsp;</td>';
1346 }
1347
1348 function _added( $lines ) {
1349 global $wgOut;
1350 foreach ($lines as $line) {
1351 $wgOut->addHTML( '<tr>' . $this->emptyLine() .
1352 $this->addedLine( htmlspecialchars ( $line ) ) . "</tr>\n" );
1353 }
1354 }
1355
1356 function _deleted($lines) {
1357 global $wgOut;
1358 foreach ($lines as $line) {
1359 $wgOut->addHTML( '<tr>' . $this->deletedLine( htmlspecialchars ( $line ) ) .
1360 $this->emptyLine() . "</tr>\n" );
1361 }
1362 }
1363
1364 function _context( $lines ) {
1365 global $wgOut;
1366 foreach ($lines as $line) {
1367 $wgOut->addHTML( '<tr>' .
1368 $this->contextLine( htmlspecialchars ( $line ) ) .
1369 $this->contextLine( htmlspecialchars ( $line ) ) . "</tr>\n" );
1370 }
1371 }
1372
1373 function _changed( $orig, $closing ) {
1374 global $wgOut;
1375 $diff = new WordLevelDiff( $orig, $closing );
1376 $del = $diff->orig();
1377 $add = $diff->closing();
1378
1379 # Notice that WordLevelDiff returns HTML-escaped output.
1380 # Hence, we will be calling addedLine/deletedLine without HTML-escaping.
1381
1382 while ( $line = array_shift( $del ) ) {
1383 $aline = array_shift( $add );
1384 $wgOut->addHTML( '<tr>' . $this->deletedLine( $line ) .
1385 $this->addedLine( $aline ) . "</tr>\n" );
1386 }
1387 foreach ($add as $line) { # If any leftovers
1388 $wgOut->addHTML( '<tr>' . $this->emptyLine() .
1389 $this->addedLine( $line ) . "</tr>\n" );
1390 }
1391 }
1392 }
1393
1394 ?>