1<?php 2/** 3 * General API for generating and formatting diffs - the differences between 4 * two sequences of strings. 5 * 6 * The original PHP version of this code was written by Geoffrey T. Dairiki 7 * <dairiki@dairiki.org>, and is used/adapted with his permission. 8 * 9 * Copyright 2004 Geoffrey T. Dairiki <dairiki@dairiki.org> 10 * Copyright 2004-2017 Horde LLC (http://www.horde.org/) 11 * 12 * See the enclosed file COPYING for license information (LGPL). If you did 13 * not receive this file, see http://www.horde.org/licenses/lgpl21. 14 * 15 * @package Text_Diff 16 * @author Geoffrey T. Dairiki <dairiki@dairiki.org> 17 */ 18class Horde_Text_Diff 19{ 20 /** 21 * Array of changes. 22 * 23 * @var array 24 */ 25 protected $_edits; 26 27 /** 28 * Computes diffs between sequences of strings. 29 * 30 * @param string $engine Name of the diffing engine to use. 'auto' 31 * will automatically select the best. 32 * @param array $params Parameters to pass to the diffing engine. 33 * Normally an array of two arrays, each 34 * containing the lines from a file. 35 */ 36 public function __construct($engine, $params) 37 { 38 if ($engine == 'auto') { 39 $engine = extension_loaded('xdiff') ? 'Xdiff' : 'Native'; 40 } else { 41 $engine = Horde_String::ucfirst(basename($engine)); 42 } 43 44 $class = 'Horde_Text_Diff_Engine_' . $engine; 45 $diff_engine = new $class(); 46 47 $this->_edits = call_user_func_array(array($diff_engine, 'diff'), $params); 48 } 49 50 /** 51 * Returns the array of differences. 52 */ 53 public function getDiff() 54 { 55 return $this->_edits; 56 } 57 58 /** 59 * returns the number of new (added) lines in a given diff. 60 * 61 * @return integer The number of new lines 62 */ 63 public function countAddedLines() 64 { 65 $count = 0; 66 foreach ($this->_edits as $edit) { 67 if ($edit instanceof Horde_Text_Diff_Op_Add || 68 $edit instanceof Horde_Text_Diff_Op_Change) { 69 $count += $edit->nfinal(); 70 } 71 } 72 return $count; 73 } 74 75 /** 76 * Returns the number of deleted (removed) lines in a given diff. 77 * 78 * @return integer The number of deleted lines 79 */ 80 public function countDeletedLines() 81 { 82 $count = 0; 83 foreach ($this->_edits as $edit) { 84 if ($edit instanceof Horde_Text_Diff_Op_Delete || 85 $edit instanceof Horde_Text_Diff_Op_Change) { 86 $count += $edit->norig(); 87 } 88 } 89 return $count; 90 } 91 92 /** 93 * Computes a reversed diff. 94 * 95 * Example: 96 * <code> 97 * $diff = new Horde_Text_Diff($lines1, $lines2); 98 * $rev = $diff->reverse(); 99 * </code> 100 * 101 * @return Horde_Text_Diff A Diff object representing the inverse of the 102 * original diff. Note that we purposely don't return a 103 * reference here, since this essentially is a clone() 104 * method. 105 */ 106 public function reverse() 107 { 108 if (version_compare(zend_version(), '2', '>')) { 109 $rev = clone($this); 110 } else { 111 $rev = $this; 112 } 113 $rev->_edits = array(); 114 foreach ($this->_edits as $edit) { 115 $rev->_edits[] = $edit->reverse(); 116 } 117 return $rev; 118 } 119 120 /** 121 * Checks for an empty diff. 122 * 123 * @return boolean True if two sequences were identical. 124 */ 125 public function isEmpty() 126 { 127 foreach ($this->_edits as $edit) { 128 if (!($edit instanceof Horde_Text_Diff_Op_Copy)) { 129 return false; 130 } 131 } 132 return true; 133 } 134 135 /** 136 * Computes the length of the Longest Common Subsequence (LCS). 137 * 138 * This is mostly for diagnostic purposes. 139 * 140 * @return integer The length of the LCS. 141 */ 142 public function lcs() 143 { 144 $lcs = 0; 145 foreach ($this->_edits as $edit) { 146 if ($edit instanceof Horde_Text_Diff_Op_Copy) { 147 $lcs += count($edit->orig); 148 } 149 } 150 return $lcs; 151 } 152 153 /** 154 * Gets the original set of lines. 155 * 156 * This reconstructs the $from_lines parameter passed to the constructor. 157 * 158 * @return array The original sequence of strings. 159 */ 160 public function getOriginal() 161 { 162 $lines = array(); 163 foreach ($this->_edits as $edit) { 164 if ($edit->orig) { 165 array_splice($lines, count($lines), 0, $edit->orig); 166 } 167 } 168 return $lines; 169 } 170 171 /** 172 * Gets the final set of lines. 173 * 174 * This reconstructs the $to_lines parameter passed to the constructor. 175 * 176 * @return array The sequence of strings. 177 */ 178 public function getFinal() 179 { 180 $lines = array(); 181 foreach ($this->_edits as $edit) { 182 if ($edit->final) { 183 array_splice($lines, count($lines), 0, $edit->final); 184 } 185 } 186 return $lines; 187 } 188 189 /** 190 * Removes trailing newlines from a line of text. This is meant to be used 191 * with array_walk(). 192 * 193 * @param string $line The line to trim. 194 * @param integer $key The index of the line in the array. Not used. 195 */ 196 public static function trimNewlines(&$line, $key) 197 { 198 $line = str_replace(array("\n", "\r"), '', $line); 199 } 200 201 /** 202 * Checks a diff for validity. 203 * 204 * This is here only for debugging purposes. 205 */ 206 protected function _check($from_lines, $to_lines) 207 { 208 if (serialize($from_lines) != serialize($this->getOriginal())) { 209 trigger_error("Reconstructed original doesn't match", E_USER_ERROR); 210 } 211 if (serialize($to_lines) != serialize($this->getFinal())) { 212 trigger_error("Reconstructed final doesn't match", E_USER_ERROR); 213 } 214 215 $rev = $this->reverse(); 216 if (serialize($to_lines) != serialize($rev->getOriginal())) { 217 trigger_error("Reversed original doesn't match", E_USER_ERROR); 218 } 219 if (serialize($from_lines) != serialize($rev->getFinal())) { 220 trigger_error("Reversed final doesn't match", E_USER_ERROR); 221 } 222 223 $prevtype = null; 224 foreach ($this->_edits as $edit) { 225 if ($prevtype == get_class($edit)) { 226 trigger_error("Edit sequence is non-optimal", E_USER_ERROR); 227 } 228 $prevtype = get_class($edit); 229 } 230 231 return true; 232 } 233} 234