source: sipes/modules_contrib/diff/DiffEngine.php @ 177a560

stableversion-3.0
Last change on this file since 177a560 was 177a560, checked in by José Gregorio Puentes <jpuentes@…>, 8 años ago

se agrego el directorio de modulos contribuidos de drupal

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