DiffLib.php 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  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. * @param array $text an entire section of a unified diff (between @@ lines)
  160. * @param char $_check a '+' or '-' denoting whether we're looking for lines
  161. * added or removed
  162. * @return
  163. */
  164. public function parseDiff($text, $_check) {
  165. $start = 0; // Start of the diff
  166. $length = 0; // number of lines to recurse
  167. $changes = array(); // all the changes
  168. $regs = array();
  169. if (preg_match("/^@@ ([\+\-])([0-9]+)(?:,([0-9]+))? [\+\-]([0-9]+)(?:,([0-9]+))? @@$/", array_shift($text), $regs) == false) {
  170. return;
  171. }
  172. $start = $regs[4];
  173. $length = count($text);
  174. $instance = new Changes();
  175. /* We don't count removed lines when looking at start of a change. For
  176. * example, we have this :
  177. * - foo
  178. * + bar
  179. * bar starts at line 1, not line 2.
  180. */
  181. $minus = 0;
  182. for ($i = 0; $i < $length; $i++) {
  183. $line = $text[$i];
  184. // empty line? EOF?
  185. if (strlen($line) == 0) {
  186. if ($instance->length > 0) {
  187. array_push($changes, $instance);
  188. $instance = new Changes();
  189. }
  190. continue;
  191. }
  192. if ($_check === '-' && $_check == $line[0]) {
  193. if ($instance->length == 0) {
  194. $instance->line = $start + $i - $minus;
  195. $instance->symbol = $line[0];
  196. $instance->length++;
  197. array_push($instance->oldline, substr($line, 1));
  198. } elseif ($_check === '+' && $_check == $line[0]) {
  199. if ($instance->length == 0) {
  200. $instance->line = $start + $i - $minus;
  201. $instance->symbol = $line[0];
  202. }
  203. $instance->length++;
  204. } else {
  205. if ($instance->length > 0) {
  206. array_push($changes, $instance);
  207. $instance = new Changes();
  208. }
  209. }
  210. if ($line[0] === '-')
  211. $minus++;
  212. }
  213. }
  214. if ($instance->length > 0) {
  215. array_push($changes, $instance);
  216. $instance = new Changes();
  217. }
  218. return $changes;
  219. }
  220. /**
  221. * Appends or Replaces text
  222. * @param array &$text Array of Line objects
  223. * @param array $change Array of Change objects
  224. * @param integer &$offset how many lines to skip due to previous additions
  225. */
  226. public function applyChange(&$text, $change, &$offset = 0) {
  227. $index = 0;
  228. // $i is the change we are on
  229. for ($i = 0; $i < count($change); $i++) {
  230. $lines = $change[$i];
  231. // $j is the line within the change
  232. for ($j = 0; $j < $lines->length; $j++) {
  233. $linenum = $lines->line - 1;
  234. $line = $text[$linenum + $j + $offset];
  235. $color = 'green';
  236. if (strlen(ltrim($line->text)) == 0) {
  237. continue;
  238. }
  239. if ($lines->symbol === '-') {
  240. $add = $lines->oldline;
  241. array_splice($text, $linenum + $j + $offset, 0, $add);
  242. // $k is the counter for the old lines we
  243. // removed from the previous version
  244. for ($k = 0; $k < count($add); $k++) {
  245. $l = new Line();
  246. $l->changed = true;
  247. $l->symbol = '-';
  248. $l->text = sprintf("%s <span class='diff-remove'>%s</span>\n", "-", rtrim($add[$k], "\r\n"));
  249. $text[$linenum + $j + $k + $offset] = $l;
  250. }
  251. $offset += count($add);
  252. } else {
  253. $l = new Line();
  254. $l->symbol = '+';
  255. $l->changed = true;
  256. $l->text = sprintf("%s <span class='diff-add'>%s</span>\n", $lines->symbol, rtrim($line->text, "\r\n"));
  257. $text[$linenum + $j] = $l;
  258. }
  259. }
  260. }
  261. }
  262. }
  263. /*** other files **/
  264. class Changes {
  265. public $line = 0;
  266. public $length = 0;
  267. public $symbol;
  268. public $oldline = array(); // only for code removed
  269. }
  270. // This object is created for every line of text in the file.
  271. // It was either this, or some funk string changes
  272. class Line {
  273. public $text = '';
  274. public $symbol = '';
  275. public $changed = false;
  276. }