1 <!-- $Id: diff.php,v 1.5 2001-02-07 18:35:09 dairiki Exp $ -->
5 // A PHP diff engine for phpwiki.
7 // Copyright (C) 2000 Geoffrey T. Dairiki <dairiki@dairiki.org>
8 // You may copy this code freely under the conditions of the GPL.
11 // FIXME: possibly remove assert()'s for production version?
13 // PHP3 does not have assert()
14 define('USE_ASSERTS', function_exists('assert'));
18 * Class used internally by WikiDiff to actually compute the diffs.
20 * The algorithm used here is mostly lifted from the perl module
21 * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
22 * http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
24 * More ideas are taken from:
25 * http://www.ics.uci.edu/~eppstein/161/960229.html
27 * Some ideas are (and a bit of code) are from from analyze.c, from GNU
28 * diffutils-2.7, which can be found at:
29 * ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
31 * Finally, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
36 var $edits; // List of editing operation to convert XV to YV.
37 var $xv = array(), $yv = array();
39 function _WikiDiffEngine ($from_lines, $to_lines)
41 $n_from = sizeof($from_lines);
42 $n_to = sizeof($to_lines);
45 // Ignore trailing and leading matching lines.
46 while ($n_from > 0 && $n_to > 0)
48 if ($from_lines[$n_from - 1] != $to_lines[$n_to - 1])
54 for ( $skip = 0; $skip < min($n_from, $n_to); $skip++)
55 if ($from_lines[$skip] != $to_lines[$skip])
63 // Ignore lines which do not exist in both files.
64 for ($x = 0; $x < $n_from; $x++)
65 $xhash[$from_lines[$x + $skip]] = 1;
66 for ($y = 0; $y < $n_to; $y++)
68 $line = $to_lines[$y + $skip];
70 if ( ($this->ychanged[$y] = empty($xhash[$line])) )
76 for ($x = 0; $x < $n_from; $x++)
78 $line = $from_lines[$x + $skip];
80 if ( ($this->xchanged[$x] = empty($yhash[$line])) )
87 $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
89 // Merge edits when possible
90 $this->_shift_boundaries($xlines, $this->xchanged, $this->ychanged);
91 $this->_shift_boundaries($ylines, $this->ychanged, $this->xchanged);
93 // Compute the edit operations.
94 $this->edits = array();
96 $this->edits[] = $skip;
100 while ($x < $n_from || $y < $n_to)
102 USE_ASSERTS && assert($y < $n_to || $this->xchanged[$x]);
103 USE_ASSERTS && assert($x < $n_from || $this->ychanged[$y]);
105 // Skip matching "snake".
109 while ( $x < $n_from && $y < $n_to
110 && !$this->xchanged[$x] && !$this->ychanged[$y])
117 $this->edits[] = $x - $x0;
122 while ($x < $n_from && $this->xchanged[$x])
128 $this->edits[] = -($x - $x0);
131 if ($this->ychanged[$y])
134 while ($y < $n_to && $this->ychanged[$y])
135 $adds[] = "" . $ylines[$y++];
136 $this->edits[] = $adds;
140 $this->edits[] = $endskip;
143 /* Divide the Largest Common Subsequence (LCS) of the sequences
144 * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
147 * Returns (LCS, PTS). LCS is the length of the LCS. PTS is an
148 * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
149 * sub sequences. The first sub-sequence is contained in [X0, X1),
150 * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on. Note
151 * that (X0, Y0) == (XOFF, YOFF) and
152 * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
154 * This function assumes that the first lines of the specified portions
155 * of the two files do not match, and likewise that the last lines do not
156 * match. The caller must trim matching lines from the beginning and end
157 * of the portions it is going to specify.
159 function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks)
161 if ($xlim - $xoff > $ylim - $yoff)
163 // Things seems faster (I'm not sure I understand why)
164 // when the shortest sequence in X.
166 list ($xoff, $xlim, $yoff, $ylim)
167 = array( $yoff, $ylim, $xoff, $xlim);
171 for ($i = $ylim - 1; $i >= $yoff; $i--)
172 $ymatches[$this->xv[$i]][] = $i;
174 for ($i = $ylim - 1; $i >= $yoff; $i--)
175 $ymatches[$this->yv[$i]][] = $i;
178 $this->seq[0]= $yoff - 1;
179 $this->in_seq = array();
182 $numer = $xlim - $xoff + $nchunks - 1;
184 for ($chunk = 0; $chunk < $nchunks; $chunk++)
187 for ($i = 0; $i <= $this->lcs; $i++)
188 $ymids[$i][$chunk-1] = $this->seq[$i];
190 $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
191 for ( ; $x < $x1; $x++)
193 $matches = $ymatches[$flip ? $this->yv[$x] : $this->xv[$x]];
197 while (list ($junk, $y) = each($matches))
198 if (! $this->in_seq[$y])
200 $k = $this->_lcs_pos($y);
201 USE_ASSERTS && assert($k > 0);
202 $ymids[$k] = $ymids[$k-1];
205 while (list ($junk, $y) = each($matches))
207 if ($y > $this->seq[$k-1])
209 USE_ASSERTS && assert($y < $this->seq[$k]);
210 // Optimization: this is a common case:
211 // next match is just replacing previous match.
212 $this->in_seq[$this->seq[$k]] = false;
214 $this->in_seq[$y] = 1;
216 else if (! $this->in_seq[$y])
218 $k = $this->_lcs_pos($y);
219 USE_ASSERTS && assert($k > 0);
220 $ymids[$k] = $ymids[$k-1];
226 $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
227 $ymid = $ymids[$this->lcs];
228 for ($n = 0; $n < $nchunks - 1; $n++)
230 $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
232 $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
234 $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
236 return array($this->lcs, $seps);
239 function _lcs_pos ($ypos)
242 if ($end == 0 || $ypos > $this->seq[$end])
244 $this->seq[++$this->lcs] = $ypos;
245 $this->in_seq[$ypos] = 1;
252 $mid = (int)(($beg + $end) / 2);
253 if ( $ypos > $this->seq[$mid] )
259 USE_ASSERTS && assert($ypos != $this->seq[$end]);
261 $this->in_seq[$this->seq[$end]] = false;
262 $this->seq[$end] = $ypos;
263 $this->in_seq[$ypos] = 1;
267 /* Find LCS of two sequences.
269 * The results are recorded in the vectors $this->{x,y}changed[], by
270 * storing a 1 in the element for each line that is an insertion
271 * or deletion (ie. is not in the LCS).
273 * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
275 * Note that XLIM, YLIM are exclusive bounds.
276 * All line numbers are origin-0 and discarded lines are not counted.
278 function _compareseq ($xoff, $xlim, $yoff, $ylim)
280 // Slide down the bottom initial diagonal.
281 while ($xoff < $xlim && $yoff < $ylim
282 && $this->xv[$xoff] == $this->yv[$yoff])
288 // Slide up the top initial diagonal.
289 while ($xlim > $xoff && $ylim > $yoff
290 && $this->xv[$xlim - 1] == $this->yv[$ylim - 1])
296 if ($xoff == $xlim || $yoff == $ylim)
300 // This is ad hoc but seems to work well.
301 //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
302 //$nchunks = max(2,min(8,(int)$nchunks));
303 $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
305 = $this->_diag($xoff,$xlim,$yoff, $ylim,$nchunks);
310 // X and Y sequences have no common subsequence:
312 while ($yoff < $ylim)
313 $this->ychanged[$this->yind[$yoff++]] = 1;
314 while ($xoff < $xlim)
315 $this->xchanged[$this->xind[$xoff++]] = 1;
319 // Use the partitions to split this problem into subproblems.
322 while ($pt2 = next($seps))
324 $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
330 /* Adjust inserts/deletes of identical lines to join changes
331 * as much as possible.
333 * We do something when a run of changed lines include a
334 * line at one end and has an excluded, identical line at the other.
335 * We are free to choose which identical line is included.
336 * `compareseq' usually chooses the one at the beginning,
337 * but usually it is cleaner to consider the following identical line
338 * to be the "change".
340 * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
342 function _shift_boundaries ($lines, &$changed, $other_changed)
347 USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
348 $len = sizeof($lines);
349 $other_len = sizeof($other_changed);
354 * Scan forwards to find beginning of another run of changes.
355 * Also keep track of the corresponding point in the other file.
357 * Throughout this code, $i and $j are adjusted together so that
358 * the first $i elements of $changed and the first $j elements
359 * of $other_changed both contain the same number of zeros
361 * Furthermore, $j is always kept so that $j == $other_len or
362 * $other_changed[$j] == false.
364 while ($j < $other_len && $other_changed[$j])
367 while ($i < $len && ! $changed[$i])
369 USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
371 while ($j < $other_len && $other_changed[$j])
380 // Find the end of this run of changes.
381 while (++$i < $len && $changed[$i])
387 * Record the length of this run of changes, so that
388 * we can later determine whether the run has grown.
390 $runlength = $i - $start;
393 * Move the changed region back, so long as the
394 * previous unchanged line matches the last changed one.
395 * This merges with previous changed regions.
397 while ($start > 0 && $lines[$start - 1] == $lines[$i - 1])
399 $changed[--$start] = 1;
400 $changed[--$i] = false;
401 while ($start > 0 && $changed[$start - 1])
403 USE_ASSERTS && assert('$j > 0');
404 while ($other_changed[--$j])
406 USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
410 * Set CORRESPONDING to the end of the changed run, at the last
411 * point where it corresponds to a changed run in the other file.
412 * CORRESPONDING == LEN means no such point has been found.
414 $corresponding = $j < $other_len ? $i : $len;
417 * Move the changed region forward, so long as the
418 * first changed line matches the following unchanged one.
419 * This merges with following changed regions.
420 * Do this second, so that if there are no merges,
421 * the changed region is moved forward as far as possible.
423 while ($i < $len && $lines[$start] == $lines[$i])
425 $changed[$start++] = false;
427 while ($i < $len && $changed[$i])
430 USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
432 if ($j < $other_len && $other_changed[$j])
435 while ($j < $other_len && $other_changed[$j])
440 while ($runlength != $i - $start);
443 * If possible, move the fully-merged run of changes
444 * back to a corresponding run in the other file.
446 while ($corresponding < $i)
448 $changed[--$start] = 1;
450 USE_ASSERTS && assert('$j > 0');
451 while ($other_changed[--$j])
453 USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
460 * Class representing a diff between two files.
467 * Compute diff between files (or deserialize serialized WikiDiff.)
469 function WikiDiff($from_lines = false, $to_lines = false)
471 if ($from_lines && $to_lines)
473 $compute = new _WikiDiffEngine($from_lines, $to_lines);
474 $this->edits = $compute->edits;
476 else if ($from_lines)
478 // $from_lines is not really from_lines, but rather
479 // a serialized WikiDiff.
480 $this->edits = unserialize($from_lines);
484 $this->edits = array();
489 * Compute reversed WikiDiff.
493 * $diff = new WikiDiff($lines1, $lines2);
494 * $rev = $diff->reverse($lines1);
496 * // reconstruct $lines1 from $lines2:
497 * $out = $rev->apply($lines2);
499 function reverse ($from_lines)
504 for ( reset($this->edits);
505 $edit = current($this->edits);
509 { // Was an add, turn it into a delete.
510 $nadd = sizeof($edit);
511 USE_ASSERTS && assert ($nadd > 0);
516 // Was a copy --- just pass it through. }
520 { // Was a delete, turn it into an add.
523 while ($ndelete-- > 0)
524 $edit[] = "" . $from_lines[$x++];
526 else die("assertion error");
528 $rev->edits[] = $edit;
535 * Compose (concatenate) WikiDiffs.
539 * $diff1 = new WikiDiff($lines1, $lines2);
540 * $diff2 = new WikiDiff($lines2, $lines3);
541 * $comp = $diff1->compose($diff2);
543 * // reconstruct $lines3 from $lines1:
544 * $out = $comp->apply($lines1);
546 function compose ($that)
551 $comp = new WikiDiff;
552 $left = current($this->edits);
553 $right = current($that->edits);
555 while ($left || $right)
557 if (!is_array($left) && $left < 0)
558 { // Left op is a delete.
560 $left = next($this->edits);
562 else if (is_array($right))
563 { // Right op is an add.
565 $right = next($that->edits);
567 else if (!$left || !$right)
568 die ("assertion error");
569 else if (!is_array($left) && $left > 0)
570 { // Left op is a copy.
571 if ($left <= abs($right))
573 $newop = $right > 0 ? $left : -$left;
576 $right = next($that->edits);
577 $left = next($this->edits);
582 $left -= abs($right);
583 $right = next($that->edits);
587 { // Left op is an add.
588 if (!is_array($left)) die('assertion error');
589 $nleft = sizeof($left);
590 if ($nleft <= abs($right))
593 { // Right op is copy
597 else // Right op is delete
603 $right = next($that->edits);
604 $left = next($this->edits);
610 for ($i = 0; $i < $right; $i++)
611 $newop[] = $left[$i];
614 for ($i = abs($right); $i < $nleft; $i++)
617 $right = next($that->edits);
628 if (is_array($op) && is_array($newop))
630 // Both $op and $newop are adds.
631 for ($i = 0; $i < sizeof($newop); $i++)
634 else if (($op > 0 && $newop > 0) || ($op < 0 && $newop < 0))
635 { // $op and $newop are both either deletes or copies.
640 $comp->edits[] = $op;
645 $comp->edits[] = $op;
654 for (reset($this->edits);
655 $edit = current($this->edits);
662 echo "Delete " . -$edit;
663 else if (is_array($edit))
665 echo "Add " . sizeof($edit) . "<ul>";
666 for ($i = 0; $i < sizeof($edit); $i++)
667 echo "<li>" . htmlspecialchars($edit[$i]);
671 die("assertion error");
678 * Apply a WikiDiff to a set of lines.
682 * $diff = new WikiDiff($lines1, $lines2);
684 * // reconstruct $lines2 from $lines1:
685 * $out = $diff->apply($lines1);
687 function apply ($from_lines)
690 $xlim = sizeof($from_lines);
692 for ( reset($this->edits);
693 $edit = current($this->edits);
699 while (list ($junk, $line) = each($edit))
704 $output[] = $from_lines[$x++];
709 ExitWiki(sprintf(gettext ("WikiDiff::apply: line count mismatch: %s != %s"), $x, $xlim));
714 * Serialize a WikiDiff.
718 * $diff = new WikiDiff($lines1, $lines2);
719 * $string = $diff->serialize;
721 * // recover WikiDiff from serialized version:
722 * $diff2 = new WikiDiff($string);
724 function serialize ()
726 return serialize($this->edits);
730 * Return true if two files were equal.
734 if (sizeof($this->edits) > 1)
736 if (sizeof($this->edits) == 0)
738 // Test for: only edit is a copy.
739 return !is_array($this->edits[0]) && $this->edits[0] > 0;
743 * Compute the length of the Longest Common Subsequence (LCS).
745 * This is mostly for diagnostic purposed.
750 for (reset($this->edits);
751 $edit = current($this->edits);
754 if (!is_array($edit) && $edit > 0)
761 * Check a WikiDiff for validity.
763 * This is here only for debugging purposes.
765 function _check ($from_lines, $to_lines)
767 $test = $this->apply($from_lines);
768 if (serialize($test) != serialize($to_lines))
769 ExitWiki(gettext ("WikiDiff::_check: failed"));
772 $prev = current($this->edits);
773 $prevtype = is_array($prev) ? 'a' : ($prev > 0 ? 'c' : 'd');
775 while ($edit = next($this->edits))
777 $type = is_array($edit) ? 'a' : ($edit > 0 ? 'c' : 'd');
778 if ( $prevtype == $type )
779 ExitWiki(gettext ("WikiDiff::_check: edit sequence is non-optimal"));
783 printf ("<strong>" . gettext ("WikiDiff Okay: LCS = %s") . "</strong>\n", $lcs);
789 * A class to format a WikiDiff as HTML.
793 * $diff = new WikiDiff($lines1, $lines2); // compute diff.
795 * $fmt = new WikiDiffFormatter;
796 * echo $fmt->format($diff, $lines1); // Output HTMLified standard diff.
798 * or to output reverse diff (diff's that would take $lines2 to $lines1):
800 * $fmt = new WikiDiffFormatter('reversed');
801 * echo $fmt->format($diff, $lines1);
803 class WikiDiffFormatter
806 var $do_reverse_diff;
807 var $context_prefix, $deletes_prefix, $adds_prefix;
809 function WikiDiffFormatter ($reverse = false)
811 $this->do_reverse_diff = $reverse;
812 $this->context_lines = 0;
813 $this->context_prefix = ' ';
814 $this->deletes_prefix = '< ';
815 $this->adds_prefix = '> ';
818 function format ($diff, $from_lines)
820 $html = '<table width="100%" bgcolor="black"' .
821 "cellspacing=2 cellpadding=2 border=0>\n";
822 $html .= $this->_format($diff->edits, $from_lines);
823 $html .= "</table>\n";
828 function _format ($edits, $from_lines)
832 $xlim = sizeof($from_lines);
835 while ($edit = current($edits))
837 if (!is_array($edit) && $edit >= 0)
838 { // Edit op is a copy.
846 // Start of an output hunk.
847 $xoff = max(0, $x - $this->context_lines);
848 $yoff = $xoff + $y - $x;
851 // Get leading context.
853 for ($i = $xoff; $i < $x; $i++)
854 $context[] = $from_lines[$i];
855 $hunk['c'] = $context;
859 { // Edit op is an add.
861 $hunk[$this->do_reverse_diff ? 'd' : 'a'] = $edit;
864 { // Edit op is a delete
867 $deletes[] = $from_lines[$x++];
868 $hunk[$this->do_reverse_diff ? 'a' : 'd'] = $deletes;
872 $next = next($edits);
875 if ( !$next || $ncopy > 2 * $this->context_lines)
877 // End of an output hunk.
881 $xend = min($x + $this->context_lines, $xlim);
884 // Get trailing context.
886 for ($i = $x; $i < $xend; $i++)
887 $context[] = $from_lines[$i];
888 $hunks[] = array('c' => $context);
891 $xlen = $xend - $xoff;
892 $ylen = $xend + $y - $x - $yoff;
893 $xbeg = $xlen ? $xoff + 1 : $xoff;
894 $ybeg = $ylen ? $yoff + 1 : $yoff;
896 if ($this->do_reverse_diff)
897 list ($xbeg, $xlen, $ybeg, $ylen)
898 = array($ybeg, $ylen, $xbeg, $xlen);
900 $html .= $this->_emit_diff($xbeg,$xlen,$ybeg,$ylen,
910 for ($i = $x; $i < $x + $ncopy; $i++)
911 $context[] = $from_lines[$i];
912 $hunk = array('c' => $context);
922 function _emit_lines($lines, $prefix, $color)
926 while (list ($junk, $line) = each($lines))
928 $html .= "<tr bgcolor=\"$color\"><td><tt>$prefix</tt>";
929 $html .= "<tt>" . htmlspecialchars($line) . "</tt></td></tr>\n";
934 function _emit_diff ($xbeg,$xlen,$ybeg,$ylen,$hunks)
936 $html = '<tr><td><table width="100%" bgcolor="white"'
937 . " cellspacing=0 border=0 cellpadding=4>\n"
938 . '<tr bgcolor="#cccccc"><td><tt>'
939 . $this->_diff_header($xbeg, $xlen, $ybeg, $ylen)
940 . "</tt></td></tr>\n<tr><td>\n"
941 . "<table width=\"100%\" cellspacing=0 border=0 cellpadding=2>\n";
943 $prefix = array('c' => $this->context_prefix,
944 'a' => $this->adds_prefix,
945 'd' => $this->deletes_prefix);
946 $color = array('c' => '#ffffff',
950 for (reset($hunks); $hunk = current($hunks); next($hunks))
952 if (!empty($hunk['c']))
953 $html .= $this->_emit_lines($hunk['c'],
954 $this->context_prefix, '#ffffff');
955 if (!empty($hunk['d']))
956 $html .= $this->_emit_lines($hunk['d'],
957 $this->deletes_prefix, '#ccffcc');
958 if (!empty($hunk['a']))
959 $html .= $this->_emit_lines($hunk['a'],
960 $this->adds_prefix, '#ffcccc');
963 $html .= "</table></td></tr></table></td></tr>\n";
967 function _diff_header ($xbeg,$xlen,$ybeg,$ylen)
969 $what = $xlen ? ($ylen ? 'c' : 'd') : 'a';
970 $xlen = $xlen > 1 ? "," . ($xbeg + $xlen - 1) : '';
971 $ylen = $ylen > 1 ? "," . ($ybeg + $ylen - 1) : '';
973 return "$xbeg$xlen$what$ybeg$ylen";
978 * A class to format a WikiDiff as a pretty HTML unified diff.
982 * $diff = new WikiDiff($lines1, $lines2); // compute diff.
984 * $fmt = new WikiUnifiedDiffFormatter;
985 * echo $fmt->format($diff, $lines1); // Output HTMLified unified diff.
987 class WikiUnifiedDiffFormatter extends WikiDiffFormatter
989 function WikiUnifiedDiffFormatter ($reverse = false, $context_lines = 3)
991 $this->do_reverse_diff = $reverse;
992 $this->context_lines = $context_lines;
993 $this->context_prefix = ' ';
994 $this->deletes_prefix = '-';
995 $this->adds_prefix = '+';
998 function _diff_header ($xbeg,$xlen,$ybeg,$ylen)
1000 $xlen = $xlen == 1 ? '' : ",$xlen";
1001 $ylen = $ylen == 1 ? '' : ",$ylen";
1003 return "@@ -$xbeg$xlen +$ybeg$ylen @@";
1009 /////////////////////////////////////////////////////////////////
1013 if (get_magic_quotes_gpc()) {
1014 $diff = stripslashes($diff);
1019 $wiki = RetrievePage($dbi, $pagename, $WikiPageStore);
1020 // $dba = OpenDataBase($ArchivePageStore);
1021 $archive= RetrievePage($dbi, $pagename, $ArchivePageStore);
1023 $html = '<table><tr><td align="right">';
1024 $html .= gettext ("Current page:");
1026 if (is_array($wiki)) {
1028 $html .= sprintf(gettext ("version %s"), $wiki['version']);
1029 $html .= "</td><td>";
1030 $html .= sprintf(gettext ("last modified on %s"),
1031 date($datetimeformat, $wiki['lastmodified']));
1032 $html .= "</td><td>";
1033 $html .= sprintf (gettext ("by %s"), $wiki['author']);
1036 $html .= "<td colspan=3><em>";
1037 $html .= gettext ("None");
1038 $html .= "</em></td>";
1041 $html .= '<tr><td align="right">';
1042 $html .= gettext ("Archived page:");
1044 if (is_array($archive)) {
1046 $html .= sprintf(gettext ("version %s"), $archive['version']);
1047 $html .= "</td><td>";
1048 $html .= sprintf(gettext ("last modified on %s"),
1049 date($datetimeformat, $archive['lastmodified']));
1050 $html .= "</td><td>";
1051 $html .= sprintf(gettext ("by %s"), $archive['author']);
1054 $html .= "<td colspan=3><em>";
1055 $html .= gettext ("None");
1056 $html .= "</em></td>";
1058 $html .= "</tr></table><p>\n";
1060 if (is_array($wiki) && is_array($archive))
1062 $diff = new WikiDiff($archive['content'], $wiki['content']);
1063 if ($diff->isEmpty()) {
1064 $html .= '<hr>[' . gettext ("Versions are identical") . ']';
1066 //$fmt = new WikiDiffFormatter;
1067 $fmt = new WikiUnifiedDiffFormatter;
1068 $html .= $fmt->format($diff, $archive['content']);
1072 GeneratePage('MESSAGE', $html, sprintf(gettext ("Diff of %s."),
1073 htmlspecialchars($pagename)), 0);