PageRenderTime 58ms CodeModel.GetById 27ms RepoModel.GetById 0ms app.codeStats 0ms

/plugins/alaxos/libs/DifferenceEngine.php

https://github.com/zpartakov/pmCake
PHP | 1080 lines | 688 code | 139 blank | 253 comment | 179 complexity | 9793eb7765048ad140dc34cc6ff3cfa5 MD5 | raw file
Possible License(s): LGPL-3.0, GPL-3.0
  1. <?php
  2. /**
  3. * A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
  4. *
  5. * Additions by Axel Boldt for MediaWiki
  6. *
  7. * @copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
  8. * @license You may copy this code freely under the conditions of the GPL.
  9. */
  10. define('USE_ASSERTS', function_exists('assert'));
  11. class _DiffOp {
  12. var $type;
  13. var $orig;
  14. var $closing;
  15. function reverse() {
  16. trigger_error("pure virtual", E_USER_ERROR);
  17. }
  18. function norig() {
  19. return $this->orig ? sizeof($this->orig) : 0;
  20. }
  21. function nclosing() {
  22. return $this->closing ? sizeof($this->closing) : 0;
  23. }
  24. }
  25. class _DiffOp_Copy extends _DiffOp {
  26. var $type = 'copy';
  27. function _DiffOp_Copy ($orig, $closing = false) {
  28. if (!is_array($closing))
  29. $closing = $orig;
  30. $this->orig = $orig;
  31. $this->closing = $closing;
  32. }
  33. function reverse() {
  34. return new _DiffOp_Copy($this->closing, $this->orig);
  35. }
  36. }
  37. class _DiffOp_Delete extends _DiffOp {
  38. var $type = 'delete';
  39. function _DiffOp_Delete ($lines) {
  40. $this->orig = $lines;
  41. $this->closing = false;
  42. }
  43. function reverse() {
  44. return new _DiffOp_Add($this->orig);
  45. }
  46. }
  47. class _DiffOp_Add extends _DiffOp {
  48. var $type = 'add';
  49. function _DiffOp_Add ($lines) {
  50. $this->closing = $lines;
  51. $this->orig = false;
  52. }
  53. function reverse() {
  54. return new _DiffOp_Delete($this->closing);
  55. }
  56. }
  57. class _DiffOp_Change extends _DiffOp {
  58. var $type = 'change';
  59. function _DiffOp_Change ($orig, $closing) {
  60. $this->orig = $orig;
  61. $this->closing = $closing;
  62. }
  63. function reverse() {
  64. return new _DiffOp_Change($this->closing, $this->orig);
  65. }
  66. }
  67. /**
  68. * Class used internally by Diff to actually compute the diffs.
  69. *
  70. * The algorithm used here is mostly lifted from the perl module
  71. * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
  72. * http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
  73. *
  74. * More ideas are taken from:
  75. * http://www.ics.uci.edu/~eppstein/161/960229.html
  76. *
  77. * Some ideas are (and a bit of code) are from from analyze.c, from GNU
  78. * diffutils-2.7, which can be found at:
  79. * ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
  80. *
  81. * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
  82. * are my own.
  83. *
  84. * @author Geoffrey T. Dairiki
  85. * @access private
  86. */
  87. class _DiffEngine
  88. {
  89. function diff ($from_lines, $to_lines) {
  90. $n_from = sizeof($from_lines);
  91. $n_to = sizeof($to_lines);
  92. $this->xchanged = $this->ychanged = array();
  93. $this->xv = $this->yv = array();
  94. $this->xind = $this->yind = array();
  95. unset($this->seq);
  96. unset($this->in_seq);
  97. unset($this->lcs);
  98. // Skip leading common lines.
  99. for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
  100. if ($from_lines[$skip] != $to_lines[$skip])
  101. break;
  102. $this->xchanged[$skip] = $this->ychanged[$skip] = false;
  103. }
  104. // Skip trailing common lines.
  105. $xi = $n_from; $yi = $n_to;
  106. for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
  107. if ($from_lines[$xi] != $to_lines[$yi])
  108. break;
  109. $this->xchanged[$xi] = $this->ychanged[$yi] = false;
  110. }
  111. // Ignore lines which do not exist in both files.
  112. for ($xi = $skip; $xi < $n_from - $endskip; $xi++)
  113. $xhash[$from_lines[$xi]] = 1;
  114. for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
  115. $line = $to_lines[$yi];
  116. if ( ($this->ychanged[$yi] = empty($xhash[$line])) )
  117. continue;
  118. $yhash[$line] = 1;
  119. $this->yv[] = $line;
  120. $this->yind[] = $yi;
  121. }
  122. for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
  123. $line = $from_lines[$xi];
  124. if ( ($this->xchanged[$xi] = empty($yhash[$line])) )
  125. continue;
  126. $this->xv[] = $line;
  127. $this->xind[] = $xi;
  128. }
  129. // Find the LCS.
  130. $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
  131. // Merge edits when possible
  132. $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
  133. $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
  134. // Compute the edit operations.
  135. $edits = array();
  136. $xi = $yi = 0;
  137. while ($xi < $n_from || $yi < $n_to) {
  138. USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
  139. USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
  140. // Skip matching "snake".
  141. $copy = array();
  142. while ( $xi < $n_from && $yi < $n_to
  143. && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
  144. $copy[] = $from_lines[$xi++];
  145. ++$yi;
  146. }
  147. if ($copy)
  148. $edits[] = new _DiffOp_Copy($copy);
  149. // Find deletes & adds.
  150. $delete = array();
  151. while ($xi < $n_from && $this->xchanged[$xi])
  152. $delete[] = $from_lines[$xi++];
  153. $add = array();
  154. while ($yi < $n_to && $this->ychanged[$yi])
  155. $add[] = $to_lines[$yi++];
  156. if ($delete && $add)
  157. $edits[] = new _DiffOp_Change($delete, $add);
  158. elseif ($delete)
  159. $edits[] = new _DiffOp_Delete($delete);
  160. elseif ($add)
  161. $edits[] = new _DiffOp_Add($add);
  162. }
  163. return $edits;
  164. }
  165. /**
  166. * Divide the Largest Common Subsequence (LCS) of the sequences
  167. * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
  168. * sized segments.
  169. *
  170. * Returns (LCS, PTS). LCS is the length of the LCS. PTS is an
  171. * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
  172. * sub sequences. The first sub-sequence is contained in [X0, X1),
  173. * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on. Note
  174. * that (X0, Y0) == (XOFF, YOFF) and
  175. * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
  176. *
  177. * This function assumes that the first lines of the specified portions
  178. * of the two files do not match, and likewise that the last lines do not
  179. * match. The caller must trim matching lines from the beginning and end
  180. * of the portions it is going to specify.
  181. */
  182. function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks) {
  183. $flip = false;
  184. if ($xlim - $xoff > $ylim - $yoff) {
  185. // Things seems faster (I'm not sure I understand why)
  186. // when the shortest sequence in X.
  187. $flip = true;
  188. list ($xoff, $xlim, $yoff, $ylim)
  189. = array( $yoff, $ylim, $xoff, $xlim);
  190. }
  191. if ($flip)
  192. for ($i = $ylim - 1; $i >= $yoff; $i--)
  193. $ymatches[$this->xv[$i]][] = $i;
  194. else
  195. for ($i = $ylim - 1; $i >= $yoff; $i--)
  196. $ymatches[$this->yv[$i]][] = $i;
  197. $this->lcs = 0;
  198. $this->seq[0]= $yoff - 1;
  199. $this->in_seq = array();
  200. $ymids[0] = array();
  201. $numer = $xlim - $xoff + $nchunks - 1;
  202. $x = $xoff;
  203. for ($chunk = 0; $chunk < $nchunks; $chunk++) {
  204. if ($chunk > 0)
  205. for ($i = 0; $i <= $this->lcs; $i++)
  206. $ymids[$i][$chunk-1] = $this->seq[$i];
  207. $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
  208. for ( ; $x < $x1; $x++) {
  209. $line = $flip ? $this->yv[$x] : $this->xv[$x];
  210. if (empty($ymatches[$line]))
  211. continue;
  212. $matches = $ymatches[$line];
  213. reset($matches);
  214. while (list ($junk, $y) = each($matches))
  215. if (empty($this->in_seq[$y])) {
  216. $k = $this->_lcs_pos($y);
  217. USE_ASSERTS && assert($k > 0);
  218. $ymids[$k] = $ymids[$k-1];
  219. break;
  220. }
  221. while (list ($junk, $y) = each($matches)) {
  222. if ($y > $this->seq[$k-1]) {
  223. USE_ASSERTS && assert($y < $this->seq[$k]);
  224. // Optimization: this is a common case:
  225. // next match is just replacing previous match.
  226. $this->in_seq[$this->seq[$k]] = false;
  227. $this->seq[$k] = $y;
  228. $this->in_seq[$y] = 1;
  229. }
  230. else if (empty($this->in_seq[$y])) {
  231. $k = $this->_lcs_pos($y);
  232. USE_ASSERTS && assert($k > 0);
  233. $ymids[$k] = $ymids[$k-1];
  234. }
  235. }
  236. }
  237. }
  238. $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
  239. $ymid = $ymids[$this->lcs];
  240. for ($n = 0; $n < $nchunks - 1; $n++) {
  241. $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
  242. $y1 = $ymid[$n] + 1;
  243. $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
  244. }
  245. $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
  246. return array($this->lcs, $seps);
  247. }
  248. function _lcs_pos ($ypos) {
  249. $end = $this->lcs;
  250. if ($end == 0 || $ypos > $this->seq[$end]) {
  251. $this->seq[++$this->lcs] = $ypos;
  252. $this->in_seq[$ypos] = 1;
  253. return $this->lcs;
  254. }
  255. $beg = 1;
  256. while ($beg < $end) {
  257. $mid = (int)(($beg + $end) / 2);
  258. if ( $ypos > $this->seq[$mid] )
  259. $beg = $mid + 1;
  260. else
  261. $end = $mid;
  262. }
  263. USE_ASSERTS && assert($ypos != $this->seq[$end]);
  264. $this->in_seq[$this->seq[$end]] = false;
  265. $this->seq[$end] = $ypos;
  266. $this->in_seq[$ypos] = 1;
  267. return $end;
  268. }
  269. /**
  270. * Find LCS of two sequences.
  271. *
  272. * The results are recorded in the vectors $this->{x,y}changed[], by
  273. * storing a 1 in the element for each line that is an insertion
  274. * or deletion (ie. is not in the LCS).
  275. *
  276. * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
  277. *
  278. * Note that XLIM, YLIM are exclusive bounds.
  279. * All line numbers are origin-0 and discarded lines are not counted.
  280. */
  281. function _compareseq ($xoff, $xlim, $yoff, $ylim) {
  282. // Slide down the bottom initial diagonal.
  283. while ($xoff < $xlim && $yoff < $ylim
  284. && $this->xv[$xoff] == $this->yv[$yoff]) {
  285. ++$xoff;
  286. ++$yoff;
  287. }
  288. // Slide up the top initial diagonal.
  289. while ($xlim > $xoff && $ylim > $yoff
  290. && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
  291. --$xlim;
  292. --$ylim;
  293. }
  294. if ($xoff == $xlim || $yoff == $ylim)
  295. $lcs = 0;
  296. else {
  297. // This is ad hoc but seems to work well.
  298. //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
  299. //$nchunks = max(2,min(8,(int)$nchunks));
  300. $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
  301. list ($lcs, $seps)
  302. = $this->_diag($xoff,$xlim,$yoff, $ylim,$nchunks);
  303. }
  304. if ($lcs == 0) {
  305. // X and Y sequences have no common subsequence:
  306. // mark all changed.
  307. while ($yoff < $ylim)
  308. $this->ychanged[$this->yind[$yoff++]] = 1;
  309. while ($xoff < $xlim)
  310. $this->xchanged[$this->xind[$xoff++]] = 1;
  311. }
  312. else {
  313. // Use the partitions to split this problem into subproblems.
  314. reset($seps);
  315. $pt1 = $seps[0];
  316. while ($pt2 = next($seps)) {
  317. $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
  318. $pt1 = $pt2;
  319. }
  320. }
  321. }
  322. /**
  323. * Adjust inserts/deletes of identical lines to join changes
  324. * as much as possible.
  325. *
  326. * We do something when a run of changed lines include a
  327. * line at one end and has an excluded, identical line at the other.
  328. * We are free to choose which identical line is included.
  329. * `compareseq' usually chooses the one at the beginning,
  330. * but usually it is cleaner to consider the following identical line
  331. * to be the "change".
  332. *
  333. * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
  334. */
  335. function _shift_boundaries ($lines, &$changed, $other_changed) {
  336. $i = 0;
  337. $j = 0;
  338. USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
  339. $len = sizeof($lines);
  340. $other_len = sizeof($other_changed);
  341. while (1) {
  342. /*
  343. * Scan forwards to find beginning of another run of changes.
  344. * Also keep track of the corresponding point in the other file.
  345. *
  346. * Throughout this code, $i and $j are adjusted together so that
  347. * the first $i elements of $changed and the first $j elements
  348. * of $other_changed both contain the same number of zeros
  349. * (unchanged lines).
  350. * Furthermore, $j is always kept so that $j == $other_len or
  351. * $other_changed[$j] == false.
  352. */
  353. while ($j < $other_len && $other_changed[$j])
  354. $j++;
  355. while ($i < $len && ! $changed[$i]) {
  356. USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
  357. $i++; $j++;
  358. while ($j < $other_len && $other_changed[$j])
  359. $j++;
  360. }
  361. if ($i == $len)
  362. break;
  363. $start = $i;
  364. // Find the end of this run of changes.
  365. while (++$i < $len && $changed[$i])
  366. continue;
  367. do {
  368. /*
  369. * Record the length of this run of changes, so that
  370. * we can later determine whether the run has grown.
  371. */
  372. $runlength = $i - $start;
  373. /*
  374. * Move the changed region back, so long as the
  375. * previous unchanged line matches the last changed one.
  376. * This merges with previous changed regions.
  377. */
  378. while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
  379. $changed[--$start] = 1;
  380. $changed[--$i] = false;
  381. while ($start > 0 && $changed[$start - 1])
  382. $start--;
  383. USE_ASSERTS && assert('$j > 0');
  384. while ($other_changed[--$j])
  385. continue;
  386. USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
  387. }
  388. /*
  389. * Set CORRESPONDING to the end of the changed run, at the last
  390. * point where it corresponds to a changed run in the other file.
  391. * CORRESPONDING == LEN means no such point has been found.
  392. */
  393. $corresponding = $j < $other_len ? $i : $len;
  394. /*
  395. * Move the changed region forward, so long as the
  396. * first changed line matches the following unchanged one.
  397. * This merges with following changed regions.
  398. * Do this second, so that if there are no merges,
  399. * the changed region is moved forward as far as possible.
  400. */
  401. while ($i < $len && $lines[$start] == $lines[$i]) {
  402. $changed[$start++] = false;
  403. $changed[$i++] = 1;
  404. while ($i < $len && $changed[$i])
  405. $i++;
  406. USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
  407. $j++;
  408. if ($j < $other_len && $other_changed[$j]) {
  409. $corresponding = $i;
  410. while ($j < $other_len && $other_changed[$j])
  411. $j++;
  412. }
  413. }
  414. } while ($runlength != $i - $start);
  415. /*
  416. * If possible, move the fully-merged run of changes
  417. * back to a corresponding run in the other file.
  418. */
  419. while ($corresponding < $i) {
  420. $changed[--$start] = 1;
  421. $changed[--$i] = 0;
  422. USE_ASSERTS && assert('$j > 0');
  423. while ($other_changed[--$j])
  424. continue;
  425. USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
  426. }
  427. }
  428. }
  429. }
  430. /**
  431. * Class representing a 'diff' between two sequences of strings.
  432. */
  433. class Diff
  434. {
  435. var $edits;
  436. /**
  437. * Constructor.
  438. * Computes diff between sequences of strings.
  439. *
  440. * @param $from_lines array An array of strings.
  441. * (Typically these are lines from a file.)
  442. * @param $to_lines array An array of strings.
  443. */
  444. function Diff($from_lines, $to_lines) {
  445. $eng = new _DiffEngine;
  446. $this->edits = $eng->diff($from_lines, $to_lines);
  447. //$this->_check($from_lines, $to_lines);
  448. }
  449. /**
  450. * Compute reversed Diff.
  451. *
  452. * SYNOPSIS:
  453. *
  454. * $diff = new Diff($lines1, $lines2);
  455. * $rev = $diff->reverse();
  456. * @return object A Diff object representing the inverse of the
  457. * original diff.
  458. */
  459. function reverse () {
  460. $rev = $this;
  461. $rev->edits = array();
  462. foreach ($this->edits as $edit) {
  463. $rev->edits[] = $edit->reverse();
  464. }
  465. return $rev;
  466. }
  467. /**
  468. * Check for empty diff.
  469. *
  470. * @return bool True iff two sequences were identical.
  471. */
  472. function isEmpty () {
  473. foreach ($this->edits as $edit) {
  474. if ($edit->type != 'copy')
  475. return false;
  476. }
  477. return true;
  478. }
  479. /**
  480. * Compute the length of the Longest Common Subsequence (LCS).
  481. *
  482. * This is mostly for diagnostic purposed.
  483. *
  484. * @return int The length of the LCS.
  485. */
  486. function lcs () {
  487. $lcs = 0;
  488. foreach ($this->edits as $edit) {
  489. if ($edit->type == 'copy')
  490. $lcs += sizeof($edit->orig);
  491. }
  492. return $lcs;
  493. }
  494. /**
  495. * Get the original set of lines.
  496. *
  497. * This reconstructs the $from_lines parameter passed to the
  498. * constructor.
  499. *
  500. * @return array The original sequence of strings.
  501. */
  502. function orig() {
  503. $lines = array();
  504. foreach ($this->edits as $edit) {
  505. if ($edit->orig)
  506. array_splice($lines, sizeof($lines), 0, $edit->orig);
  507. }
  508. return $lines;
  509. }
  510. /**
  511. * Get the closing set of lines.
  512. *
  513. * This reconstructs the $to_lines parameter passed to the
  514. * constructor.
  515. *
  516. * @return array The sequence of strings.
  517. */
  518. function closing() {
  519. $lines = array();
  520. foreach ($this->edits as $edit) {
  521. if ($edit->closing)
  522. array_splice($lines, sizeof($lines), 0, $edit->closing);
  523. }
  524. return $lines;
  525. }
  526. /**
  527. * Check a Diff for validity.
  528. *
  529. * This is here only for debugging purposes.
  530. */
  531. function _check ($from_lines, $to_lines) {
  532. if (serialize($from_lines) != serialize($this->orig()))
  533. trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
  534. if (serialize($to_lines) != serialize($this->closing()))
  535. trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
  536. $rev = $this->reverse();
  537. if (serialize($to_lines) != serialize($rev->orig()))
  538. trigger_error("Reversed original doesn't match", E_USER_ERROR);
  539. if (serialize($from_lines) != serialize($rev->closing()))
  540. trigger_error("Reversed closing doesn't match", E_USER_ERROR);
  541. $prevtype = 'none';
  542. foreach ($this->edits as $edit) {
  543. if ( $prevtype == $edit->type )
  544. trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
  545. $prevtype = $edit->type;
  546. }
  547. $lcs = $this->lcs();
  548. trigger_error("Diff okay: LCS = $lcs", E_USER_NOTICE);
  549. }
  550. }
  551. /**
  552. * FIXME: bad name.
  553. */
  554. class MappedDiff
  555. extends Diff
  556. {
  557. /**
  558. * Constructor.
  559. *
  560. * Computes diff between sequences of strings.
  561. *
  562. * This can be used to compute things like
  563. * case-insensitve diffs, or diffs which ignore
  564. * changes in white-space.
  565. *
  566. * @param $from_lines array An array of strings.
  567. * (Typically these are lines from a file.)
  568. *
  569. * @param $to_lines array An array of strings.
  570. *
  571. * @param $mapped_from_lines array This array should
  572. * have the same size number of elements as $from_lines.
  573. * The elements in $mapped_from_lines and
  574. * $mapped_to_lines are what is actually compared
  575. * when computing the diff.
  576. *
  577. * @param $mapped_to_lines array This array should
  578. * have the same number of elements as $to_lines.
  579. */
  580. function MappedDiff($from_lines, $to_lines,
  581. $mapped_from_lines, $mapped_to_lines) {
  582. assert(sizeof($from_lines) == sizeof($mapped_from_lines));
  583. assert(sizeof($to_lines) == sizeof($mapped_to_lines));
  584. $this->Diff($mapped_from_lines, $mapped_to_lines);
  585. $xi = $yi = 0;
  586. for ($i = 0; $i < sizeof($this->edits); $i++) {
  587. $orig = &$this->edits[$i]->orig;
  588. if (is_array($orig)) {
  589. $orig = array_slice($from_lines, $xi, sizeof($orig));
  590. $xi += sizeof($orig);
  591. }
  592. $closing = &$this->edits[$i]->closing;
  593. if (is_array($closing)) {
  594. $closing = array_slice($to_lines, $yi, sizeof($closing));
  595. $yi += sizeof($closing);
  596. }
  597. }
  598. }
  599. }
  600. /**
  601. * A class to format Diffs
  602. *
  603. * This class formats the diff in classic diff format.
  604. * It is intended that this class be customized via inheritance,
  605. * to obtain fancier outputs.
  606. */
  607. class DiffFormatter
  608. {
  609. /**
  610. * Number of leading context "lines" to preserve.
  611. *
  612. * This should be left at zero for this class, but subclasses
  613. * may want to set this to other values.
  614. */
  615. var $leading_context_lines = 0;
  616. /**
  617. * Number of trailing context "lines" to preserve.
  618. *
  619. * This should be left at zero for this class, but subclasses
  620. * may want to set this to other values.
  621. */
  622. var $trailing_context_lines = 0;
  623. /**
  624. * Format a diff.
  625. *
  626. * @param $diff object A Diff object.
  627. * @return string The formatted output.
  628. */
  629. function format($diff) {
  630. $xi = $yi = 1;
  631. $block = false;
  632. $context = array();
  633. $nlead = $this->leading_context_lines;
  634. $ntrail = $this->trailing_context_lines;
  635. $this->_start_diff();
  636. foreach ($diff->edits as $edit) {
  637. if ($edit->type == 'copy') {
  638. if (is_array($block)) {
  639. if (sizeof($edit->orig) <= $nlead + $ntrail) {
  640. $block[] = $edit;
  641. }
  642. else{
  643. if ($ntrail) {
  644. $context = array_slice($edit->orig, 0, $ntrail);
  645. $block[] = new _DiffOp_Copy($context);
  646. }
  647. $this->_block($x0, $ntrail + $xi - $x0,
  648. $y0, $ntrail + $yi - $y0,
  649. $block);
  650. $block = false;
  651. }
  652. }
  653. $context = $edit->orig;
  654. }
  655. else {
  656. if (! is_array($block)) {
  657. $context = array_slice($context, sizeof($context) - $nlead);
  658. $x0 = $xi - sizeof($context);
  659. $y0 = $yi - sizeof($context);
  660. $block = array();
  661. if ($context)
  662. $block[] = new _DiffOp_Copy($context);
  663. }
  664. $block[] = $edit;
  665. }
  666. if ($edit->orig)
  667. $xi += sizeof($edit->orig);
  668. if ($edit->closing)
  669. $yi += sizeof($edit->closing);
  670. }
  671. if (is_array($block))
  672. $this->_block($x0, $xi - $x0,
  673. $y0, $yi - $y0,
  674. $block);
  675. return $this->_end_diff();
  676. }
  677. function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
  678. $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
  679. foreach ($edits as $edit) {
  680. if ($edit->type == 'copy')
  681. $this->_context($edit->orig);
  682. elseif ($edit->type == 'add')
  683. $this->_added($edit->closing);
  684. elseif ($edit->type == 'delete')
  685. $this->_deleted($edit->orig);
  686. elseif ($edit->type == 'change')
  687. $this->_changed($edit->orig, $edit->closing);
  688. else
  689. trigger_error("Unknown edit type", E_USER_ERROR);
  690. }
  691. $this->_end_block();
  692. }
  693. function _start_diff() {
  694. ob_start();
  695. }
  696. function _end_diff() {
  697. $val = ob_get_contents();
  698. ob_end_clean();
  699. return $val;
  700. }
  701. function _block_header($xbeg, $xlen, $ybeg, $ylen) {
  702. if ($xlen > 1)
  703. $xbeg .= "," . ($xbeg + $xlen - 1);
  704. if ($ylen > 1)
  705. $ybeg .= "," . ($ybeg + $ylen - 1);
  706. return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
  707. }
  708. function _start_block($header) {
  709. echo $header;
  710. }
  711. function _end_block() {
  712. }
  713. function _lines($lines, $prefix = ' ') {
  714. foreach ($lines as $line)
  715. echo "$prefix $line\n";
  716. }
  717. function _context($lines) {
  718. $this->_lines($lines);
  719. }
  720. function _added($lines) {
  721. $this->_lines($lines, ">");
  722. }
  723. function _deleted($lines) {
  724. $this->_lines($lines, "<");
  725. }
  726. function _changed($orig, $closing) {
  727. $this->_deleted($orig);
  728. echo "---\n";
  729. $this->_added($closing);
  730. }
  731. }
  732. /**
  733. * Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
  734. *
  735. */
  736. define('NBSP', "\xC2\xA0"); // utf-8 non-breaking space.
  737. class _HWLDF_WordAccumulator {
  738. function _HWLDF_WordAccumulator () {
  739. $this->_lines = array();
  740. $this->_line = '';
  741. $this->_group = '';
  742. $this->_tag = '';
  743. }
  744. function _flushGroup ($new_tag) {
  745. if ($this->_group !== '') {
  746. if ($this->_tag == 'mark')
  747. $this->_line .= '<strong>'.$this->_group.'</strong>';
  748. else
  749. $this->_line .= $this->_group;
  750. }
  751. $this->_group = '';
  752. $this->_tag = $new_tag;
  753. }
  754. function _flushLine ($new_tag) {
  755. $this->_flushGroup($new_tag);
  756. if ($this->_line != '')
  757. $this->_lines[] = $this->_line;
  758. $this->_line = '';
  759. }
  760. function addWords ($words, $tag = '') {
  761. if ($tag != $this->_tag)
  762. $this->_flushGroup($tag);
  763. foreach ($words as $word) {
  764. // new-line should only come as first char of word.
  765. if ($word == '')
  766. continue;
  767. if ($word[0] == "\n") {
  768. $this->_group .= NBSP;
  769. $this->_flushLine($tag);
  770. $word = substr($word, 1);
  771. }
  772. assert(!strstr($word, "\n"));
  773. $this->_group .= $word;
  774. }
  775. }
  776. function getLines() {
  777. $this->_flushLine('~done');
  778. return $this->_lines;
  779. }
  780. }
  781. class WordLevelDiff extends MappedDiff
  782. {
  783. function WordLevelDiff ($orig_lines, $closing_lines) {
  784. list ($orig_words, $orig_stripped) = $this->_split($orig_lines);
  785. list ($closing_words, $closing_stripped) = $this->_split($closing_lines);
  786. $this->MappedDiff($orig_words, $closing_words,
  787. $orig_stripped, $closing_stripped);
  788. }
  789. function _split($lines) {
  790. // FIXME: fix POSIX char class.
  791. # if (!preg_match_all('/ ( [^\S\n]+ | [[:alnum:]]+ | . ) (?: (?!< \n) [^\S\n])? /xs',
  792. if (!preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs',
  793. implode("\n", $lines),
  794. $m)) {
  795. return array(array(''), array(''));
  796. }
  797. return array($m[0], $m[1]);
  798. }
  799. function orig () {
  800. $orig = new _HWLDF_WordAccumulator;
  801. foreach ($this->edits as $edit) {
  802. if ($edit->type == 'copy')
  803. $orig->addWords($edit->orig);
  804. elseif ($edit->orig)
  805. $orig->addWords($edit->orig, 'mark');
  806. }
  807. return $orig->getLines();
  808. }
  809. function closing () {
  810. $closing = new _HWLDF_WordAccumulator;
  811. foreach ($this->edits as $edit) {
  812. if ($edit->type == 'copy')
  813. $closing->addWords($edit->closing);
  814. elseif ($edit->closing)
  815. $closing->addWords($edit->closing, 'mark');
  816. }
  817. return $closing->getLines();
  818. }
  819. }
  820. /**
  821. * "Unified" diff formatter.
  822. *
  823. * This class formats the diff in classic "unified diff" format.
  824. */
  825. class UnifiedDiffFormatter extends DiffFormatter
  826. {
  827. function UnifiedDiffFormatter($context_lines = 4) {
  828. $this->leading_context_lines = $context_lines;
  829. $this->trailing_context_lines = $context_lines;
  830. }
  831. function _block_header($xbeg, $xlen, $ybeg, $ylen) {
  832. if ($xlen != 1)
  833. $xbeg .= "," . $xlen;
  834. if ($ylen != 1)
  835. $ybeg .= "," . $ylen;
  836. return "@@ -$xbeg +$ybeg @@\n";
  837. }
  838. function _added($lines) {
  839. $this->_lines($lines, "+");
  840. }
  841. function _deleted($lines) {
  842. $this->_lines($lines, "-");
  843. }
  844. function _changed($orig, $final) {
  845. $this->_deleted($orig);
  846. $this->_added($final);
  847. }
  848. }
  849. /**
  850. * Wikipedia Table style diff formatter.
  851. *
  852. */
  853. class TableDiffFormatter extends DiffFormatter
  854. {
  855. var $line_name = 'line';
  856. function TableDiffFormatter() {
  857. $this->leading_context_lines = 2;
  858. $this->trailing_context_lines = 2;
  859. }
  860. function _pre($text){
  861. $text = htmlspecialchars($text);
  862. $text = str_replace(' ',' &nbsp;',$text);
  863. if($text{0} == ' ') $text = '&nbsp;'.substr($text,1);
  864. return $text;
  865. }
  866. function _block_header( $xbeg, $xlen, $ybeg, $ylen ) {
  867. global $lang;
  868. // $l1 = $lang['line'].' '.$xbeg;
  869. // $l2 = $lang['line'].' '.$ybeg;
  870. $l1 = $this->line_name .' '.$xbeg;
  871. $l2 = $this->line_name .' '.$ybeg;
  872. $r = '<tr><td class="diff-blockheader" colspan="2">'.$l1.":</td>\n" .
  873. '<td class="diff-blockheader" colspan="2">'.$l2.":</td></tr>\n";
  874. return $r;
  875. }
  876. function _start_block( $header ) {
  877. print( $header );
  878. }
  879. function _end_block() {
  880. }
  881. function _lines( $lines, $prefix=' ', $color="white" ) {
  882. }
  883. function addedLine( $line )
  884. {
  885. $line = str_replace(' ',' &nbsp;',$line);
  886. if(strlen($line) > 0 && $line{0} == ' ')
  887. {
  888. $line = '&nbsp;'.substr($line,1);
  889. }
  890. return '<td>+</td><td class="diff-addedline">' . $line.'</td>';
  891. }
  892. function deletedLine( $line ) {
  893. $line = str_replace(' ',' &nbsp;',$line);
  894. if($line{0} == ' ') $line = '&nbsp;'.substr($line,1);
  895. return '<td>-</td><td class="diff-deletedline">' .
  896. $line.'</td>';
  897. }
  898. function emptyLine() {
  899. //$line = str_replace(' ','&nbsp; ',$line);
  900. return '<td colspan="2">&nbsp;</td>';
  901. }
  902. function contextLine( $line )
  903. {
  904. $line = str_replace(' ',' &nbsp;', $line);
  905. if(strlen($line) > 0 && $line{0} == ' ')
  906. {
  907. $line = '&nbsp;'.substr($line,1);
  908. }
  909. return '<td> </td><td class="diff-context">'.$line.'</td>';
  910. }
  911. function _added($lines) {
  912. foreach ($lines as $line) {
  913. print( '<tr>' . $this->emptyLine() .
  914. $this->addedLine( $line ) . "</tr>\n" );
  915. }
  916. }
  917. function _deleted($lines) {
  918. foreach ($lines as $line) {
  919. print( '<tr>' . $this->deletedLine( $line ) .
  920. $this->emptyLine() . "</tr>\n" );
  921. }
  922. }
  923. function _context( $lines ) {
  924. foreach ($lines as $line) {
  925. print( '<tr>' . $this->contextLine( $line ) .
  926. $this->contextLine( $line ) . "</tr>\n" );
  927. }
  928. }
  929. function _changed( $orig, $closing ) {
  930. $diff = new WordLevelDiff( $orig, $closing );
  931. $del = $diff->orig();
  932. $add = $diff->closing();
  933. while ( $line = array_shift( $del ) ) {
  934. $aline = array_shift( $add );
  935. print( '<tr>' . $this->deletedLine( $line ) .
  936. $this->addedLine( $aline ) . "</tr>\n" );
  937. }
  938. $this->_added( $add ); # If any leftovers
  939. }
  940. }
  941. //Setup VIM: ex: et ts=2 enc=utf-8 :