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-2012 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: */
18: class 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: * @since Text_Diff 1.1.0
62: *
63: * @return integer The number of new lines
64: */
65: public function countAddedLines()
66: {
67: $count = 0;
68: foreach ($this->_edits as $edit) {
69: if ($edit instanceof Horde_Text_Diff_Op_Add ||
70: $edit instanceof Horde_Text_Diff_Op_Change) {
71: $count += $edit->nfinal();
72: }
73: }
74: return $count;
75: }
76:
77: /**
78: * Returns the number of deleted (removed) lines in a given diff.
79: *
80: * @since Text_Diff 1.1.0
81: *
82: * @return integer The number of deleted lines
83: */
84: public function countDeletedLines()
85: {
86: $count = 0;
87: foreach ($this->_edits as $edit) {
88: if ($edit instanceof Horde_Text_Diff_Op_Delete ||
89: $edit instanceof Horde_Text_Diff_Op_Change) {
90: $count += $edit->norig();
91: }
92: }
93: return $count;
94: }
95:
96: /**
97: * Computes a reversed diff.
98: *
99: * Example:
100: * <code>
101: * $diff = new Horde_Text_Diff($lines1, $lines2);
102: * $rev = $diff->reverse();
103: * </code>
104: *
105: * @return Horde_Text_Diff A Diff object representing the inverse of the
106: * original diff. Note that we purposely don't return a
107: * reference here, since this essentially is a clone()
108: * method.
109: */
110: public function reverse()
111: {
112: if (version_compare(zend_version(), '2', '>')) {
113: $rev = clone($this);
114: } else {
115: $rev = $this;
116: }
117: $rev->_edits = array();
118: foreach ($this->_edits as $edit) {
119: $rev->_edits[] = $edit->reverse();
120: }
121: return $rev;
122: }
123:
124: /**
125: * Checks for an empty diff.
126: *
127: * @return boolean True if two sequences were identical.
128: */
129: public function isEmpty()
130: {
131: foreach ($this->_edits as $edit) {
132: if (!($edit instanceof Horde_Text_Diff_Op_Copy)) {
133: return false;
134: }
135: }
136: return true;
137: }
138:
139: /**
140: * Computes the length of the Longest Common Subsequence (LCS).
141: *
142: * This is mostly for diagnostic purposes.
143: *
144: * @return integer The length of the LCS.
145: */
146: public function lcs()
147: {
148: $lcs = 0;
149: foreach ($this->_edits as $edit) {
150: if ($edit instanceof Horde_Text_Diff_Op_Copy) {
151: $lcs += count($edit->orig);
152: }
153: }
154: return $lcs;
155: }
156:
157: /**
158: * Gets the original set of lines.
159: *
160: * This reconstructs the $from_lines parameter passed to the constructor.
161: *
162: * @return array The original sequence of strings.
163: */
164: public function getOriginal()
165: {
166: $lines = array();
167: foreach ($this->_edits as $edit) {
168: if ($edit->orig) {
169: array_splice($lines, count($lines), 0, $edit->orig);
170: }
171: }
172: return $lines;
173: }
174:
175: /**
176: * Gets the final set of lines.
177: *
178: * This reconstructs the $to_lines parameter passed to the constructor.
179: *
180: * @return array The sequence of strings.
181: */
182: public function getFinal()
183: {
184: $lines = array();
185: foreach ($this->_edits as $edit) {
186: if ($edit->final) {
187: array_splice($lines, count($lines), 0, $edit->final);
188: }
189: }
190: return $lines;
191: }
192:
193: /**
194: * Removes trailing newlines from a line of text. This is meant to be used
195: * with array_walk().
196: *
197: * @param string $line The line to trim.
198: * @param integer $key The index of the line in the array. Not used.
199: */
200: static public function trimNewlines(&$line, $key)
201: {
202: $line = str_replace(array("\n", "\r"), '', $line);
203: }
204:
205: /**
206: * Checks a diff for validity.
207: *
208: * This is here only for debugging purposes.
209: */
210: protected function _check($from_lines, $to_lines)
211: {
212: if (serialize($from_lines) != serialize($this->getOriginal())) {
213: trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
214: }
215: if (serialize($to_lines) != serialize($this->getFinal())) {
216: trigger_error("Reconstructed final doesn't match", E_USER_ERROR);
217: }
218:
219: $rev = $this->reverse();
220: if (serialize($to_lines) != serialize($rev->getOriginal())) {
221: trigger_error("Reversed original doesn't match", E_USER_ERROR);
222: }
223: if (serialize($from_lines) != serialize($rev->getFinal())) {
224: trigger_error("Reversed final doesn't match", E_USER_ERROR);
225: }
226:
227: $prevtype = null;
228: foreach ($this->_edits as $edit) {
229: if ($prevtype == get_class($edit)) {
230: trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
231: }
232: $prevtype = get_class($edit);
233: }
234:
235: return true;
236: }
237: }
238: