DiffLib.php 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. <?php
  2. /**
  3. * A wrapper class to generate diffs.
  4. *
  5. * Text:
  6. * - Context
  7. * - Unified
  8. * HTML:
  9. * - Inline
  10. * - SideBySide
  11. *
  12. * Currently uses Horde classes, but could also use PEAR or php-diff packages.
  13. *
  14. * @author Mark Scherer
  15. * @license MIT
  16. */
  17. class DiffLib {
  18. /**
  19. * What engine should Text_Diff use.
  20. * Avaible: auto (chooses best), native, xdiff
  21. *
  22. * @var string
  23. */
  24. public $engine = 'auto';
  25. # xdiff needs external libs
  26. public $engines = array('auto', 'native', 'shell', 'xdiff');
  27. /**
  28. * What renderer to use
  29. * for avaible renderers look in Text/Diff/Renderer/*
  30. * Standard: unified, context, inline
  31. * Additional: sidebyside?
  32. *
  33. * @var string
  34. */
  35. public $renderer = 'inline';
  36. public $renderers = array('inline', 'unified', 'context'); //'sidebyside'
  37. /**
  38. * Do you want to use the Character diff renderer additionally to the sidebyside renderer ?
  39. * sidebyside renderer is the only one supporting the additional renderer
  40. *
  41. * @var boolean
  42. */
  43. public $characterDiff = true;
  44. /**
  45. * If the params are strings on what characters do you want to explode the string?
  46. * Can be an array if you want to explode on multiple chars
  47. *
  48. * @var mixed
  49. */
  50. public $explodeOn = "\r\n";
  51. /**
  52. * How many context lines do you want to see around the changed line?
  53. *
  54. * @var integer
  55. */
  56. public $contextLines = 4;
  57. /**
  58. * DiffLib::__construct()
  59. *
  60. */
  61. public function __construct() {
  62. set_include_path(get_include_path() . PATH_SEPARATOR . CakePlugin::path('Tools') . 'Vendor' . DS);
  63. App::import('Vendor', 'Tools.HordeAutoloader', array('file' => 'Horde/Autoloader/Default.php'));
  64. }
  65. /**
  66. * Set/Get renderer
  67. *
  68. * @param string $renderType
  69. * 'unified', 'inline', 'context', 'sidebyside'
  70. * defaults to "inline"
  71. * @return boolean Success
  72. */
  73. public function renderType($type = null) {
  74. if ($type === null) {
  75. return $this->renderer;
  76. }
  77. if (in_array($type, $this->renderers)) {
  78. $this->renderer = $type;
  79. return true;
  80. }
  81. return false;
  82. }
  83. /**
  84. * Set/Get engine
  85. *
  86. * @param string $engineType
  87. * 'auto', 'native', 'xdiff', 'shell', 'string'
  88. * defaults to "auto"
  89. * @return boolean Success
  90. */
  91. public function engineType($type = null) {
  92. if ($type === null) {
  93. return $this->engine;
  94. }
  95. if (in_array($type, $this->engines)) {
  96. $this->engine = $type;
  97. return true;
  98. }
  99. return false;
  100. }
  101. /**
  102. * Compare function
  103. * Compares two strings/arrays using the specified method and renderer
  104. *
  105. * @param mixed $original
  106. * @param mixed $changed
  107. * @param array $options
  108. * - div: true/false
  109. * - class: defaults to "diff"
  110. * - escape: defaults to true
  111. * @return string output
  112. */
  113. public function compare($original, $changed, $options = array()) {
  114. if (!is_array($original)) {
  115. $original = $this->_explode($original);
  116. }
  117. if (!is_array($changed)) {
  118. $changed = $this->_explode($changed);
  119. }
  120. $rendererClassName = 'Horde_Text_Diff_Renderer_' . ucfirst($this->renderer);
  121. $renderer = new $rendererClassName(array('context_lines' => $this->contextLines, 'character_diff' => $this->characterDiff));
  122. $diff = new Horde_Text_Diff($this->engine, array($original, $changed));
  123. $string = $renderer->render($diff);
  124. return $string;
  125. }
  126. /**
  127. * @param string $string Either context or unified diff snippet
  128. * @param array $options
  129. * - mode (autodetect, context, unified)
  130. */
  131. public function reverse($string, $options = array()) {
  132. $defaults = array(
  133. 'mode' => 'autodetect',
  134. );
  135. $options += $defaults;
  136. $diff = new Horde_Text_Diff('string', array($string, $options['mode']));
  137. $rendererClassName = 'Horde_Text_Diff_Renderer_' . ucfirst($this->renderer);
  138. $renderer = new $rendererClassName(array('context_lines' => $this->contextLines, 'character_diff' => $this->characterDiff));
  139. $string = $renderer->render($diff);
  140. return $string;
  141. }
  142. /**
  143. * Explodes the string into an array
  144. *
  145. * @param string $text
  146. * @return array
  147. */
  148. protected function _explode($text) {
  149. if (is_array($this->explodeOn)) {
  150. foreach ($this->explodeOn as $explodeOn) {
  151. $text = explode($explodeOn, $text);
  152. }
  153. return $text;
  154. }
  155. return explode($this->explodeOn, $text);
  156. }
  157. /**
  158. * Parses a unified diff output
  159. *
  160. * @param array $text an entire section of a unified diff (between @@ lines)
  161. * @param char $_check a '+' or '-' denoting whether we're looking for lines
  162. * added or removed
  163. * @return
  164. */
  165. public function parseDiff($text, $_check) {
  166. $start = 0; // Start of the diff
  167. $length = 0; // number of lines to recurse
  168. $changes = array(); // all the changes
  169. $regs = array();
  170. if (preg_match("/^@@ ([\+\-])([0-9]+)(?:,([0-9]+))? [\+\-]([0-9]+)(?:,([0-9]+))? @@$/", array_shift($text), $regs) == false) {
  171. return;
  172. }
  173. $start = $regs[4];
  174. $length = count($text);
  175. $instance = new Changes();
  176. /* We don't count removed lines when looking at start of a change. For
  177. * example, we have this :
  178. * - foo
  179. * + bar
  180. * bar starts at line 1, not line 2.
  181. */
  182. $minus = 0;
  183. for ($i = 0; $i < $length; $i++) {
  184. $line = $text[$i];
  185. // empty line? EOF?
  186. if (strlen($line) === 0) {
  187. if ($instance->length > 0) {
  188. array_push($changes, $instance);
  189. $instance = new Changes();
  190. }
  191. continue;
  192. }
  193. if ($_check === '-' && $_check == $line[0]) {
  194. if ($instance->length == 0) {
  195. $instance->line = $start + $i - $minus;
  196. $instance->symbol = $line[0];
  197. $instance->length++;
  198. array_push($instance->oldline, substr($line, 1));
  199. } elseif ($_check === '+' && $_check == $line[0]) {
  200. if ($instance->length == 0) {
  201. $instance->line = $start + $i - $minus;
  202. $instance->symbol = $line[0];
  203. }
  204. $instance->length++;
  205. } else {
  206. if ($instance->length > 0) {
  207. array_push($changes, $instance);
  208. $instance = new Changes();
  209. }
  210. }
  211. if ($line[0] === '-')
  212. $minus++;
  213. }
  214. }
  215. if ($instance->length > 0) {
  216. array_push($changes, $instance);
  217. $instance = new Changes();
  218. }
  219. return $changes;
  220. }
  221. /**
  222. * Appends or Replaces text
  223. *
  224. * @param array &$text Array of Line objects
  225. * @param array $change Array of Change objects
  226. * @param integer &$offset how many lines to skip due to previous additions
  227. */
  228. public function applyChange(&$text, $change, &$offset = 0) {
  229. $index = 0;
  230. // $i is the change we are on
  231. for ($i = 0; $i < count($change); $i++) {
  232. $lines = $change[$i];
  233. // $j is the line within the change
  234. for ($j = 0; $j < $lines->length; $j++) {
  235. $linenum = $lines->line - 1;
  236. $line = $text[$linenum + $j + $offset];
  237. $color = 'green';
  238. if (strlen(ltrim($line->text)) === 0) {
  239. continue;
  240. }
  241. if ($lines->symbol === '-') {
  242. $add = $lines->oldline;
  243. array_splice($text, $linenum + $j + $offset, 0, $add);
  244. // $k is the counter for the old lines we
  245. // removed from the previous version
  246. for ($k = 0; $k < count($add); $k++) {
  247. $l = new Line();
  248. $l->changed = true;
  249. $l->symbol = '-';
  250. $l->text = sprintf("%s <span class='diff-remove'>%s</span>\n", "-", rtrim($add[$k], "\r\n"));
  251. $text[$linenum + $j + $k + $offset] = $l;
  252. }
  253. $offset += count($add);
  254. } else {
  255. $l = new Line();
  256. $l->symbol = '+';
  257. $l->changed = true;
  258. $l->text = sprintf("%s <span class='diff-add'>%s</span>\n", $lines->symbol, rtrim($line->text, "\r\n"));
  259. $text[$linenum + $j] = $l;
  260. }
  261. }
  262. }
  263. }
  264. }
  265. /*** other files **/
  266. class Changes {
  267. public $line = 0;
  268. public $length = 0;
  269. public $symbol;
  270. public $oldline = array(); // only for code removed
  271. }
  272. // This object is created for every line of text in the file.
  273. // It was either this, or some funk string changes
  274. class Line {
  275. public $text = '';
  276. public $symbol = '';
  277. public $changed = false;
  278. }