DiffLib.php 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  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 $character_diff = 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 $explode_on = "\r\n";
  51. /**
  52. * How many context lines do you want to see around the changed line?
  53. *
  54. * @var integer
  55. */
  56. public $context_lines = 4;
  57. /**
  58. * DiffLib::__construct()
  59. *
  60. * @return void
  61. */
  62. public function __construct() {
  63. set_include_path(get_include_path() . PATH_SEPARATOR . CakePlugin::path('Tools') . 'Vendor' . DS);
  64. App::import('Vendor', 'Tools.HordeAutoloader', array('file'=> 'Horde/Autoloader/Default.php'));
  65. }
  66. /**
  67. * Set/Get renderer
  68. *
  69. * @param string $renderType
  70. * 'unified', 'inline', 'context', 'sidebyside'
  71. * defaults to "inline"
  72. * @return boolean Success
  73. */
  74. public function renderType($type = null) {
  75. if ($type === null) {
  76. return $this->renderer;
  77. }
  78. if (in_array($type, $this->renderers)) {
  79. $this->renderer = $type;
  80. return true;
  81. }
  82. return false;
  83. }
  84. /**
  85. * Set/Get engine
  86. *
  87. * @param string $engineType
  88. * 'auto', 'native', 'xdiff', 'shell', 'string'
  89. * defaults to "auto"
  90. * @return boolean Success
  91. */
  92. public function engineType($type = null) {
  93. if ($type === null) {
  94. return $this->engine;
  95. }
  96. if (in_array($type, $this->engines)) {
  97. $this->engine = $type;
  98. return true;
  99. }
  100. return false;
  101. }
  102. /**
  103. * Compare function
  104. * Compares two strings/arrays using the specified method and renderer
  105. *
  106. * @param mixed $original
  107. * @param mixed $changed
  108. * @param array $options
  109. * - div: true/false
  110. * - class: defaults to "diff"
  111. * - escape: defaults to true
  112. * @return string output
  113. */
  114. public function compare($original, $changed, $options = array()) {
  115. if (!is_array($original)) {
  116. $original = $this->_explode($original);
  117. }
  118. if (!is_array($changed)) {
  119. $changed = $this->_explode($changed);
  120. }
  121. $rendererClassName = 'Horde_Text_Diff_Renderer_'.ucfirst($this->renderer);
  122. $renderer = new $rendererClassName(array('context_lines' => $this->context_lines, 'character_diff' =>$this->character_diff));
  123. $diff = new Horde_Text_Diff($this->engine, array($original, $changed));
  124. $string = $renderer->render($diff);
  125. return $string;
  126. }
  127. /**
  128. * @param string $string Either context or unified diff snippet
  129. * @param array $options
  130. * - mode (autodetect, context, unified)
  131. */
  132. public function reverse($string, $options = array()) {
  133. $defaults = array(
  134. 'mode' => 'autodetect',
  135. );
  136. $options += $defaults;
  137. $diff = new Horde_Text_Diff('string', array($string, $options['mode']));
  138. $rendererClassName = 'Horde_Text_Diff_Renderer_'.ucfirst($this->renderer);
  139. $renderer = new $rendererClassName(array('context_lines' => $this->context_lines, 'character_diff' =>$this->character_diff));
  140. $string = $renderer->render($diff);
  141. return $string;
  142. }
  143. /**
  144. * Explodes the string into an array
  145. *
  146. * @param string $text
  147. * @return array
  148. */
  149. protected function _explode($text) {
  150. if (is_array($this->explode_on)) {
  151. foreach ($this->explode_on as $explode_on) {
  152. $text = explode($explode_on, $text);
  153. }
  154. return $text;
  155. }
  156. return explode($this->explode_on, $text);
  157. }
  158. /**
  159. * Parses a unified diff output
  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. * @param array &$text Array of Line objects
  224. * @param array $change Array of Change objects
  225. * @param integer &$offset how many lines to skip due to previous additions
  226. */
  227. public function applyChange(&$text, $change, &$offset = 0) {
  228. $index = 0;
  229. // $i is the change we are on
  230. for ($i = 0; $i < count($change); $i++) {
  231. $lines = $change[$i];
  232. // $j is the line within the change
  233. for ($j = 0; $j < $lines->length; $j++) {
  234. $linenum = $lines->line - 1;
  235. $line = $text[$linenum+$j+$offset];
  236. $color = 'green';
  237. if (strlen(ltrim($line->text)) == 0) {
  238. continue;
  239. }
  240. if ($lines->symbol === '-') {
  241. $add = $lines->oldline;
  242. array_splice($text, $linenum + $j + $offset, 0, $add);
  243. // $k is the counter for the old lines we
  244. // removed from the previous version
  245. for ($k = 0; $k < count($add); $k++) {
  246. $l = new Line();
  247. $l->changed = true;
  248. $l->symbol = '-';
  249. $l->text = sprintf("%s <span class='diff-remove'>%s</span>\n", "-", rtrim($add[$k], "\r\n"));
  250. $text[$linenum+$j+$k+$offset] = $l;
  251. }
  252. $offset += count($add);
  253. } else {
  254. $l = new Line();
  255. $l->symbol = '+';
  256. $l->changed = true;
  257. $l->text = sprintf("%s <span class='diff-add'>%s</span>\n", $lines->symbol, rtrim($line->text, "\r\n"));
  258. $text[$linenum+$j] = $l;
  259. }
  260. }
  261. }
  262. }
  263. }
  264. /*** other files **/
  265. class Changes {
  266. public $line = 0;
  267. public $length = 0;
  268. public $symbol;
  269. public $oldline = array(); // only for code removed
  270. }
  271. // This object is created for every line of text in the file.
  272. // It was either this, or some funk string changes
  273. class Line {
  274. public $text = '';
  275. public $symbol = '';
  276. public $changed = false;
  277. }