DiffLib.php 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  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 http://opensource.org/licenses/mit-license.php 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 = ['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 = ['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 bool
  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 int
  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', ['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 bool 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 bool 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, array $options = []) {
  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(['context_lines' => $this->contextLines, 'character_diff' => $this->characterDiff]);
  122. $diff = new Horde_Text_Diff($this->engine, [$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, array $options = []) {
  132. $defaults = [
  133. 'mode' => 'autodetect',
  134. ];
  135. $options += $defaults;
  136. $diff = new Horde_Text_Diff('string', [$string, $options['mode']]);
  137. $rendererClassName = 'Horde_Text_Diff_Renderer_' . ucfirst($this->renderer);
  138. $renderer = new $rendererClassName(['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 = []; // all the changes
  169. $regs = [];
  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. }
  216. if ($instance->length > 0) {
  217. array_push($changes, $instance);
  218. $instance = new Changes();
  219. }
  220. return $changes;
  221. }
  222. /**
  223. * Appends or Replaces text
  224. *
  225. * @param array &$text Array of Line objects
  226. * @param array $change Array of Change objects
  227. * @param int &$offset how many lines to skip due to previous additions
  228. */
  229. public function applyChange(&$text, $change, &$offset = 0) {
  230. $index = 0;
  231. // $i is the change we are on
  232. for ($i = 0; $i < count($change); $i++) {
  233. $lines = $change[$i];
  234. // $j is the line within the change
  235. for ($j = 0; $j < $lines->length; $j++) {
  236. $linenum = $lines->line - 1;
  237. $line = $text[$linenum + $j + $offset];
  238. $color = 'green';
  239. if (strlen(ltrim($line->text)) === 0) {
  240. continue;
  241. }
  242. if ($lines->symbol === '-') {
  243. $add = $lines->oldline;
  244. array_splice($text, $linenum + $j + $offset, 0, $add);
  245. // $k is the counter for the old lines we
  246. // removed from the previous version
  247. for ($k = 0; $k < count($add); $k++) {
  248. $l = new Line();
  249. $l->changed = true;
  250. $l->symbol = '-';
  251. $l->text = sprintf("%s <span class='diff-remove'>%s</span>\n", "-", rtrim($add[$k], "\r\n"));
  252. $text[$linenum + $j + $k + $offset] = $l;
  253. }
  254. $offset += count($add);
  255. } else {
  256. $l = new Line();
  257. $l->symbol = '+';
  258. $l->changed = true;
  259. $l->text = sprintf("%s <span class='diff-add'>%s</span>\n", $lines->symbol, rtrim($line->text, "\r\n"));
  260. $text[$linenum + $j] = $l;
  261. }
  262. }
  263. }
  264. }
  265. }
  266. /*** other files **/
  267. class Changes {
  268. public $line = 0;
  269. public $length = 0;
  270. public $symbol;
  271. public $oldline = []; // only for code removed
  272. }
  273. // This object is created for every line of text in the file.
  274. // It was either this, or some funk string changes
  275. class Line {
  276. public $text = '';
  277. public $symbol = '';
  278. public $changed = false;
  279. }